Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions dashboard/e2e/tree-compare.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ test('loads revisions and comparison data from the API', async ({ page }) => {

await expect(page.getByText('Tree summary')).toBeVisible();
await expect(page.getByText('Changed results')).toBeVisible();
await expect(page.getByRole('combobox', { name: 'From' })).toBeVisible();
await expect(page.getByRole('combobox', { name: 'To' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Regressions' })).toBeVisible();
await expect(page.getByRole('button', { name: 'Add' })).toBeVisible();
await expect(
page.getByRole('button', { name: 'Remove PASS to FAIL filter' }),
).toBeVisible();
await expect(page.getByText('defconfig+allmodconfig')).toBeVisible();
await expect(page.getByText('Regression')).toBeVisible();
});
11 changes: 11 additions & 0 deletions dashboard/src/locales/messages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,17 @@ export const messages = {
'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',
'treeCompare.statusPairFilter.glossary':
'PASS — completed successfully{br}' +
'FAIL — completed with a failure{br}' +
'INCONCLUSIVE — no definitive pass or fail result{br}' +
'— — absent on this side',
'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',
Expand Down
53 changes: 34 additions & 19 deletions dashboard/src/pages/TreeCompare/TreeComparePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,24 @@ import {
} from '@/api/treeCompare';

import {
compareDefaultChangeFilters,
compareNavigateFrom,
compareRouteName,
type CompareBootFailureRow,
type CompareChangeFilter,
type CompareStatusPair,
type CompareTestFailureRow,
} from '@/types/tree/TreeCompare';
import type { PossibleTabs } from '@/types/tree/TreeDetails';
import {
applyChangeFilter,
applyStatusPairFilter,
mapBootOrTestDiffRows,
mapBuildDiffRows,
readStoredStatusPairs,
resolveStatusPairs,
serializeStatusPairs,
writeStoredStatusPairs,
} from '@/utils/treeCompareDiff';

import { CompareChangeFilterBar } from './components/CompareChangeStats';
import { CompareStatusPairFilter } from './components/CompareStatusPairFilter';
import {
CompareBootsFailuresTable,
CompareBuildsFailuresTable,
Expand All @@ -59,7 +62,7 @@ const SHORT_HASH_LENGTH = 7;
const TreeComparePage = (): JSX.Element => {
const { formatMessage } = useIntl();
const { treeName, branch } = useParams({ from: compareRouteName });
const { hashA, hashB, origin, currentPageTab, changeFilter } = useSearch({
const { hashA, hashB, origin, currentPageTab, statusPair } = useSearch({
from: compareRouteName,
});
const navigate = useNavigate({ from: compareNavigateFrom });
Expand Down Expand Up @@ -109,7 +112,7 @@ const TreeComparePage = (): JSX.Element => {
hashA?: string;
hashB?: string;
currentPageTab?: PossibleTabs;
changeFilter?: CompareChangeFilter[];
statusPair?: string[];
}) => {
navigate({
search: previous => ({
Expand All @@ -120,10 +123,7 @@ const TreeComparePage = (): JSX.Element => {
updates.currentPageTab ??
previous.currentPageTab ??
'global.builds',
changeFilter:
updates.changeFilter ??
previous.changeFilter ??
compareDefaultChangeFilters,
statusPair: updates.statusPair ?? previous.statusPair,
}),
params: { treeName, branch },
resetScroll: false,
Expand Down Expand Up @@ -192,17 +192,32 @@ const TreeComparePage = (): JSX.Element => {
[testsDiffQuery.data],
);

const storedStatusPairs = useMemo(() => readStoredStatusPairs(), []);
const statusPairs = useMemo(
() => resolveStatusPairs(statusPair, storedStatusPairs),
[statusPair, storedStatusPairs],
);

const onStatusPairsChange = useCallback(
(value: CompareStatusPair[]) => {
const serialized = serializeStatusPairs(value);
writeStoredStatusPairs(serialized);
updateSearch({ statusPair: serialized });
},
[updateSearch],
);

const filteredBuilds = useMemo(
() => applyChangeFilter(buildRows, changeFilter),
[buildRows, changeFilter],
() => applyStatusPairFilter(buildRows, statusPairs),
[buildRows, statusPairs],
);
const filteredBoots = useMemo(
() => applyChangeFilter(bootRows, changeFilter),
[bootRows, changeFilter],
() => applyStatusPairFilter(bootRows, statusPairs),
[bootRows, statusPairs],
);
const filteredTests = useMemo(
() => applyChangeFilter(testRows, changeFilter),
[changeFilter, testRows],
() => applyStatusPairFilter(testRows, statusPairs),
[statusPairs, testRows],
);

const tabs: ITabItem[] = useMemo(
Expand Down Expand Up @@ -386,9 +401,9 @@ const TreeComparePage = (): JSX.Element => {
<FormattedMessage id="treeCompare.drilldownHint" />
</div>
<div className="mb-4">
<CompareChangeFilterBar
value={changeFilter}
onChange={value => updateSearch({ changeFilter: value })}
<CompareStatusPairFilter
value={statusPairs}
onChange={onStatusPairsChange}
/>
</div>
<Tabs
Expand Down
69 changes: 0 additions & 69 deletions dashboard/src/pages/TreeCompare/components/CompareChangeStats.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import type { JSX } from 'react';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are those .stories. files running in prod?

import { useState } from 'react';

import type { Meta, StoryObj } from '@storybook/react';
import { expect, fn, userEvent, within } from '@storybook/test';
import { IntlProvider } from 'react-intl';

import { LOCALES } from '@/locales/constants';
import { messages } from '@/locales/messages';

import { TooltipProvider } from '@/components/Tooltip';

import {
CompareStatusPairChip,
CompareStatusPairFilter,
CompareStatusSelect,
type CompareStatusPair,
} from './CompareStatusPairFilter';

const DEFAULT_PAIRS: CompareStatusPair[] = [
{ from: 'PASS', to: 'FAIL' },
{ from: 'FAIL', to: 'PASS' },
];

const meta: Meta<typeof CompareStatusPairFilter> = {
title: 'Tree Compare/Status Pair Filter',
component: CompareStatusPairFilter,
parameters: {
layout: 'centered',
},
tags: ['autodocs'],
decorators: [
(Story): JSX.Element => (
<IntlProvider messages={messages[LOCALES.EN_US]} locale={LOCALES.EN_US}>
<TooltipProvider>
<div className="w-[min(40rem,90vw)]">
<Story />
</div>
</TooltipProvider>
</IntlProvider>
),
],
args: {
value: DEFAULT_PAIRS,
onChange: fn(),
},
};

export default meta;
type Story = StoryObj<typeof meta>;

function StatefulFilter({
initialValue,
onChange,
}: {
initialValue: CompareStatusPair[];
onChange: (value: CompareStatusPair[]) => void;
}): JSX.Element {
const [value, setValue] = useState(initialValue);

return (
<CompareStatusPairFilter
value={value}
onChange={nextValue => {
setValue(nextValue);
onChange(nextValue);
}}
/>
);
}

export const Default: Story = {
render: args => (
<StatefulFilter initialValue={[...args.value]} onChange={args.onChange} />
),
};

export const Empty: Story = {
args: {
value: [],
},
render: args => <StatefulFilter initialValue={[]} onChange={args.onChange} />,
};

export const DuplicateGuard: Story = {
render: args => (
<StatefulFilter initialValue={[...args.value]} onChange={args.onChange} />
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const page = within(document.body);

await userEvent.click(canvas.getByRole('combobox', { name: 'From' }));
await userEvent.click(page.getByRole('option', { name: 'PASS' }));
await userEvent.click(canvas.getByRole('combobox', { name: 'To' }));
await userEvent.click(page.getByRole('option', { name: 'FAIL' }));

const addButton = canvas.getByRole('button', { name: 'Add' });
await expect(addButton).toBeDisabled();

await userEvent.click(
canvas.getByRole('button', { name: 'Remove PASS to FAIL filter' }),
);
await expect(addButton).toBeEnabled();

await userEvent.click(addButton);
await expect(
canvas.getByRole('button', { name: 'Remove PASS to FAIL filter' }),
).toBeInTheDocument();
},
};

export const StatusSelect: Story = {
render: function Render() {
const [value, setValue] = useState<CompareStatusPair['from']>();
return (
<CompareStatusSelect
id="status-select-story"
label="From"
value={value}
onChange={setValue}
/>
);
},
};

export const StatusPairChip: Story = {
render: args => (
<CompareStatusPairChip
pair={{ from: 'PASS', to: 'FAIL' }}
onRemove={() => args.onChange([])}
/>
),
};
Loading
Loading