diff --git a/dashboard/e2e/tree-compare.spec.ts b/dashboard/e2e/tree-compare.spec.ts index 3d2fddcb2..19a4fd1d6 100644 --- a/dashboard/e2e/tree-compare.spec.ts +++ b/dashboard/e2e/tree-compare.spec.ts @@ -138,7 +138,7 @@ test('loads comparison data and opens a side-by-side details drawer', async ({ ); await page.goto( - `/tree/linux/master/compare?hashA=${HASH_A}&hashB=${HASH_B}&origin=maestro`, + `/tree/linux/master/compare?range=${HASH_A}..${HASH_B}&origin=maestro`, ); await expect(page.getByText('Tree summary')).toBeVisible(); @@ -304,7 +304,7 @@ test('drawer next stays on searched rows', async ({ page }) => { ); await page.goto( - `/tree/linux/master/compare?hashA=${HASH_A}&hashB=${HASH_B}&origin=maestro`, + `/tree/linux/master/compare?range=${HASH_A}..${HASH_B}&origin=maestro`, ); await page.getByPlaceholder('Search').fill('keep-'); diff --git a/dashboard/src/locales/messages/index.ts b/dashboard/src/locales/messages/index.ts index 5d2dada70..7804ebcfd 100644 --- a/dashboard/src/locales/messages/index.ts +++ b/dashboard/src/locales/messages/index.ts @@ -417,6 +417,7 @@ export const messages = { 'tree.path': 'Trees', 'tree.searchPlaceholder': 'Search by tree, branch or tag with a regex', 'treeCompare.backToDetails': 'Back to tree details', + 'treeCompare.base': 'Base', 'treeCompare.breadcrumb': 'Compare', 'treeCompare.breakdownTitle': 'Changed results', 'treeCompare.change.appeared': 'Appeared', @@ -431,11 +432,12 @@ export const messages = { 'treeCompare.change.stillFailing': 'Still failing', 'treeCompare.change.unchanged': 'Unchanged', 'treeCompare.changed': 'Changed', + 'treeCompare.compare': 'Compare', 'treeCompare.description': - 'Compare pass/fail counts between two revisions on the same tree and branch.', - 'treeCompare.detail.missingSide': 'No result on this side', + 'Compare pass/fail counts of a revision against a base revision on the same tree and branch.', + 'treeCompare.detail.missingSide': 'No result on this revision', 'treeCompare.drilldownHint': - 'Individual builds, boots, and tests that changed between Side A and Side B.', + 'Individual builds, boots, and tests that changed from the base revision to the compared revision.', 'treeCompare.failures.change': 'Change', 'treeCompare.failures.configArch': 'Config / Arch', 'treeCompare.failures.pathArch': 'Path / Arch', @@ -454,8 +456,6 @@ export const messages = { 'Select two different revisions to compare. This tree needs at least two commits with results.', 'treeCompare.openCompare': 'Compare revisions', 'treeCompare.selectRevision': 'Select a revision', - 'treeCompare.sideA': 'Side A', - 'treeCompare.sideB': 'Side B', 'treeCompare.statusPairFilter.active': 'Active status pair filters', 'treeCompare.statusPairFilter.add': 'Add', 'treeCompare.statusPairFilter.from': 'From', @@ -463,13 +463,13 @@ export const messages = { 'PASS — completed successfully{br}' + 'FAIL — completed with a failure{br}' + 'INCONCLUSIVE — no definitive pass or fail result{br}' + - '— — absent on this side', + '— — absent on this revision', 'treeCompare.statusPairFilter.remove': 'Remove {from} to {to} filter', 'treeCompare.statusPairFilter.select': 'Select status', 'treeCompare.statusPairFilter.to': 'To', 'treeCompare.suggestion.branchHead': 'Branch head', 'treeCompare.suggestion.previous': 'Previous commit', - 'treeCompare.suggestion.swap': 'Swap sides', + 'treeCompare.suggestion.swap': 'Swap base and compare', 'treeCompare.suggestions': 'Suggestions', 'treeCompare.summaryTitle': 'Tree summary', 'treeDetails.bootsHistory': 'Boots History', diff --git a/dashboard/src/pages/TreeCompare/TreeCompareLink.tsx b/dashboard/src/pages/TreeCompare/TreeCompareLink.tsx index 48806f30b..14931daf0 100644 --- a/dashboard/src/pages/TreeCompare/TreeCompareLink.tsx +++ b/dashboard/src/pages/TreeCompare/TreeCompareLink.tsx @@ -6,6 +6,8 @@ import { FormattedMessage } from 'react-intl'; import { Button } from '@/components/ui/button'; +import { formatCompareRange } from '@/utils/treeCompareDiff'; + interface TreeCompareLinkProps { treeName: string; branch: string; @@ -24,7 +26,10 @@ export function TreeCompareLink({ s} > diff --git a/dashboard/src/pages/TreeCompare/TreeComparePage.tsx b/dashboard/src/pages/TreeCompare/TreeComparePage.tsx index 078b279c5..aea738ac8 100644 --- a/dashboard/src/pages/TreeCompare/TreeComparePage.tsx +++ b/dashboard/src/pages/TreeCompare/TreeComparePage.tsx @@ -41,10 +41,14 @@ import { } from '@/types/tree/TreeCompare'; import type { PossibleTabs } from '@/types/tree/TreeDetails'; import { + type CompareRange, applyStatusPairFilter, + formatCompareRange, mapBootOrTestDiffRows, mapBuildDiffRows, + parseCompareRange, readStoredStatusPairs, + resolveCompareRange, resolveStatusPairs, serializeStatusPairs, writeStoredStatusPairs, @@ -57,16 +61,20 @@ import { } from './components/CompareFailuresTables'; import { CompareTestsGroupedTable } from './components/CompareTestsGroupedTable'; import { CompareSummary } from './components/CompareSummary'; -import { RevisionSelectorBar } from './components/RevisionSelector'; +import { + RevisionSelectorBar, + type RevisionSide, +} from './components/RevisionSelector'; const SHORT_HASH_LENGTH = 7; const TreeComparePage = (): JSX.Element => { const { formatMessage } = useIntl(); const { treeName, branch } = useParams({ from: compareRouteName }); - const { hashA, hashB, origin, currentPageTab, statusPair } = useSearch({ + const { range, origin, currentPageTab, statusPair } = useSearch({ from: compareRouteName, }); + const { base: baseHash, compare: compareHash } = parseCompareRange(range); const navigate = useNavigate({ from: compareNavigateFrom }); const commitsQuery = useCommits({ @@ -88,19 +96,19 @@ const TreeComparePage = (): JSX.Element => { [commitsQuery.data], ); - const resolvedHashA = hashA || revisions[0]?.hash || ''; - const resolvedHashB = - hashB || - revisions.find(revision => revision.hash !== resolvedHashA)?.hash || - ''; + const { base: resolvedBase, compare: resolvedCompare } = useMemo( + () => + resolveCompareRange(revisions, { base: baseHash, compare: compareHash }), + [revisions, baseHash, compareHash], + ); - const canCompare = Boolean(resolvedHashA && resolvedHashB); + const canCompare = Boolean(resolvedBase && resolvedCompare); const compareParams = { treeName, branch, - hashA: resolvedHashA, - hashB: resolvedHashB, + hashA: resolvedBase, + hashB: resolvedCompare, origin, }; @@ -111,16 +119,17 @@ const TreeComparePage = (): JSX.Element => { const updateSearch = useCallback( (updates: { - hashA?: string; - hashB?: string; + range?: Partial; currentPageTab?: PossibleTabs; statusPair?: string[]; }) => { navigate({ search: previous => ({ ...previous, - hashA: updates.hashA ?? previous.hashA, - hashB: updates.hashB ?? previous.hashB, + range: formatCompareRange({ + ...parseCompareRange(previous.range), + ...updates.range, + }), currentPageTab: updates.currentPageTab ?? previous.currentPageTab ?? @@ -135,18 +144,18 @@ const TreeComparePage = (): JSX.Element => { ); useEffect(() => { - if ((!hashA || !hashB) && resolvedHashA && resolvedHashB) { - updateSearch({ hashA: resolvedHashA, hashB: resolvedHashB }); + if ((!baseHash || !compareHash) && resolvedBase && resolvedCompare) { + updateSearch({ range: { base: resolvedBase, compare: resolvedCompare } }); } - }, [hashA, hashB, resolvedHashA, resolvedHashB, updateSearch]); + }, [baseHash, compareHash, resolvedBase, resolvedCompare, updateSearch]); const handleSwap = useCallback(() => { - updateSearch({ hashA: resolvedHashB, hashB: resolvedHashA }); - }, [resolvedHashA, resolvedHashB, updateSearch]); + updateSearch({ range: { base: resolvedCompare, compare: resolvedBase } }); + }, [resolvedBase, resolvedCompare, updateSearch]); const handleSideAction = useCallback( - (side: 'A' | 'B', action: 'previous' | 'branchHead') => { - const currentHash = side === 'A' ? resolvedHashA : resolvedHashB; + (side: RevisionSide, action: 'previous' | 'branchHead') => { + const currentHash = side === 'base' ? resolvedBase : resolvedCompare; const currentIndex = revisions.findIndex(r => r.hash === currentHash); if (action === 'previous') { @@ -155,22 +164,14 @@ const TreeComparePage = (): JSX.Element => { Math.max(currentIndex, 0) + 1, ); const nextHash = revisions[previousIndex]?.hash ?? currentHash; - if (side === 'A') { - updateSearch({ hashA: nextHash }); - } else { - updateSearch({ hashB: nextHash }); - } + updateSearch({ range: { [side]: nextHash } }); return; } const headHash = revisions[0]?.hash ?? currentHash; - if (side === 'A') { - updateSearch({ hashA: headHash }); - } else { - updateSearch({ hashB: headHash }); - } + updateSearch({ range: { [side]: headHash } }); }, - [revisions, resolvedHashA, resolvedHashB, updateSearch], + [revisions, resolvedBase, resolvedCompare, updateSearch], ); const buildRows = useMemo( @@ -317,7 +318,7 @@ const TreeComparePage = (): JSX.Element => { params={{ treeName, branch, - hash: resolvedHashA, + hash: resolvedCompare, }} state={s => s} > @@ -345,8 +346,8 @@ const TreeComparePage = (): JSX.Element => { origin, gitUrl: compareQuery.data?.gitUrl, branch, - hashA: resolvedHashA || undefined, - hashB: resolvedHashB || undefined, + hashA: resolvedBase || undefined, + hashB: resolvedCompare || undefined, omittedFilters: statusPairs.length > 0 ? ['status-pair filter'] : [], })} @@ -362,7 +363,7 @@ const TreeComparePage = (): JSX.Element => { s} > @@ -375,11 +376,13 @@ const TreeComparePage = (): JSX.Element => { error={commitsQuery.error} > updateSearch({ hashA: value })} - onHashBChange={value => updateSearch({ hashB: value })} + onBaseChange={value => updateSearch({ range: { base: value } })} + onCompareChange={value => + updateSearch({ range: { compare: value } }) + } onSideAction={handleSideAction} onSwap={handleSwap} /> diff --git a/dashboard/src/pages/TreeCompare/components/CompareDetailSheet.tsx b/dashboard/src/pages/TreeCompare/components/CompareDetailSheet.tsx index fba25970d..013309b6b 100644 --- a/dashboard/src/pages/TreeCompare/components/CompareDetailSheet.tsx +++ b/dashboard/src/pages/TreeCompare/components/CompareDetailSheet.tsx @@ -64,7 +64,7 @@ function SideColumn({ logData, isLoading, }: { - labelId: 'treeCompare.sideA' | 'treeCompare.sideB'; + labelId: 'treeCompare.base' | 'treeCompare.compare'; status: CompareItemStatus; id: string | null; logType: LogType; @@ -154,7 +154,7 @@ export function CompareDetailSheet({
setSort(current => cycleSort(current, key))} @@ -276,7 +276,7 @@ export function CompareBuildsFailuresTable({ setSort(current => cycleSort(current, key))} @@ -408,7 +408,7 @@ export function CompareBootsFailuresTable({ /> setSort(current => cycleSort(current, key))} @@ -416,7 +416,7 @@ export function CompareBootsFailuresTable({ setSort(current => cycleSort(current, key))} diff --git a/dashboard/src/pages/TreeCompare/components/CompareTestsGroupedTable.tsx b/dashboard/src/pages/TreeCompare/components/CompareTestsGroupedTable.tsx index 7de4d463c..79956cef3 100644 --- a/dashboard/src/pages/TreeCompare/components/CompareTestsGroupedTable.tsx +++ b/dashboard/src/pages/TreeCompare/components/CompareTestsGroupedTable.tsx @@ -232,7 +232,7 @@ function createCompareSideColumns( sideStatus(row, 'sideA'), header: ({ column }): JSX.Element => (
- +
), cell: ({ row }): JSX.Element | null => { @@ -273,7 +273,7 @@ function createCompareSideColumns( sideStatus(row, 'sideB'), header: ({ column }): JSX.Element => (
- +
), cell: ({ row }): JSX.Element | null => { diff --git a/dashboard/src/pages/TreeCompare/components/RevisionSelector.tsx b/dashboard/src/pages/TreeCompare/components/RevisionSelector.tsx index ca68b07fb..93ad42641 100644 --- a/dashboard/src/pages/TreeCompare/components/RevisionSelector.tsx +++ b/dashboard/src/pages/TreeCompare/components/RevisionSelector.tsx @@ -16,7 +16,7 @@ import type { CompareRevision } from '@/types/tree/TreeCompare'; import { cn } from '@/lib/utils'; -type RevisionSide = 'A' | 'B'; +export type RevisionSide = 'base' | 'compare'; function TagChips({ tags }: { tags: string[] }): JSX.Element | null { if (tags.length === 0) { @@ -58,24 +58,14 @@ function RevisionCard({
-
- - {side} - - - - -
+ + +