Skip to content

Commit ac348de

Browse files
feat: adding test-run home page (#609)
* feat: adding test-run home page * feat: adding 2 more icons & finishing integrating homepage for testrun * add filters & paginations to testruns * adding details pages * finishing test run preview * adding testrun section to scenario homepage * adding prod test run repository * adding all filters to testrun homepage * finishing test-run home & details design + addin uuid hadnling for testrun * feat: adding first chart (implemented through divs) * feat: improve first chart by handling error cases * feat: adding filter transaction by decision chart design * feat: finishing 2 columns from last chart of test run details page * feat: add chart for rule by execution * feat: adding toggle rules changed * refactor: handling all conversation made on the PR * chore: removing lightweight chart for the moment * fix: adding a missing key to a list of components * fix: handling eslint errors * chore: update traduction for french and arabic languauge * fix: prettier run * chore: remove specs for radiogroup * fix: ts whining * fix: testRunStatus typescript whingin * fix: add scheduled-execution icon svg * style: update colors for filter transactions by decisions * WIP: identify issue with "V" prefix * comment * changes to api spec * fix: update oas spec & adding missing languages * debugging: tentative changes * fix: handling only one version on distribution of decision chart * fix: handling no rule_id for filter transactions by decisions * fix count bug * fix: warning from react * chore: update traduction for french and arabic languauge --------- Co-authored-by: Pascal Delange <[email protected]>
1 parent b466411 commit ac348de

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3013
-136
lines changed

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,4 @@
4141
"[jsonc]": {
4242
"editor.defaultFormatter": "esbenp.prettier-vscode"
4343
}
44-
}
44+
}

packages/app-builder/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@
8686
"nanoid": "^5.0.7",
8787
"pretty-cache-header": "^1.0.0",
8888
"qs": "^6.13.0",
89+
"radash": "^12.1.0",
8990
"react": "^18.3.1",
9091
"react-day-picker": "^9.1.4",
9192
"react-dom": "^18.3.1",
@@ -102,6 +103,7 @@
102103
"swagger-ui-react": "^5.17.14",
103104
"temporal-polyfill": "^0.2.5",
104105
"tiny-invariant": "^1.3.3",
106+
"ts-pattern": "^5.5.0",
105107
"typescript-utils": "workspace:*",
106108
"ui-design-system": "workspace:*",
107109
"ui-icons": "workspace:*",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { formatDateTime, useFormatLanguage } from '@app-builder/utils/format';
2+
import { Popover, PopoverDisclosure, PopoverProvider } from '@ariakit/react';
3+
import { getInputProps, useField, useInputControl } from '@conform-to/react';
4+
import clsx from 'clsx';
5+
import { type ElementRef, forwardRef, useState } from 'react';
6+
import { Button, Calendar, type Input } from 'ui-design-system';
7+
import { Icon } from 'ui-icons';
8+
9+
interface FormDateSelectorProps
10+
extends React.ComponentPropsWithoutRef<typeof Input> {
11+
name: string;
12+
description?: string;
13+
}
14+
15+
export const FormDateSelector = forwardRef<
16+
ElementRef<typeof Input>,
17+
FormDateSelectorProps
18+
>(function FormDateSelector({ name, description, ...props }, ref) {
19+
const [field, _] = useField<string>(name);
20+
const [selectedDate, selectDate] = useState<Date>();
21+
const [open, setOpen] = useState(false);
22+
const input = useInputControl(field);
23+
const language = useFormatLanguage();
24+
25+
const handleSelect = (date?: Date) => {
26+
if (date) {
27+
selectDate(date);
28+
input.change(date.toISOString());
29+
setOpen(false);
30+
}
31+
};
32+
33+
return (
34+
<div className="flex flex-row items-center gap-2">
35+
<input
36+
{...getInputProps(field, { type: 'hidden' })}
37+
ref={ref}
38+
value={selectedDate?.toISOString()}
39+
readOnly
40+
{...props}
41+
/>
42+
<PopoverProvider open={open} setOpen={setOpen}>
43+
<PopoverDisclosure render={<Button variant="secondary" />}>
44+
<Icon
45+
icon="calendar-month"
46+
className={clsx('size-6', {
47+
'text-grey-100': selectedDate,
48+
'text-grey-50': !selectedDate,
49+
})}
50+
/>
51+
<span
52+
className={clsx('font-normal', {
53+
'text-grey-100': selectedDate,
54+
'text-grey-50': !selectedDate,
55+
})}
56+
>
57+
{selectedDate
58+
? formatDateTime(selectedDate, {
59+
language,
60+
timeStyle: undefined,
61+
})
62+
: props.placeholder}
63+
</span>
64+
</PopoverDisclosure>
65+
<Popover
66+
className="bg-grey-00 border-grey-05 isolate rounded-md border p-4"
67+
gutter={8}
68+
>
69+
<Calendar
70+
mode="single"
71+
hidden={{ before: new Date() }}
72+
selected={selectedDate}
73+
onSelect={handleSelect}
74+
/>
75+
</Popover>
76+
</PopoverProvider>
77+
</div>
78+
);
79+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useOrganizationUsers } from '@app-builder/services/organization/organization-users';
2+
import { matchSorter } from '@app-builder/utils/search';
3+
import { useDeferredValue, useMemo, useState } from 'react';
4+
import { Avatar, Input, SelectWithCombobox } from 'ui-design-system';
5+
6+
import { useCreatorFilter } from '../TestRunsFiltersContext';
7+
8+
export function CreatorsFilter() {
9+
const [value, setSearchValue] = useState('');
10+
const { creator, setCreator } = useCreatorFilter();
11+
const deferredValue = useDeferredValue(value);
12+
const { orgUsers } = useOrganizationUsers();
13+
14+
const matches = useMemo(
15+
() =>
16+
matchSorter(orgUsers, deferredValue, { keys: ['firstName', 'lastName'] }),
17+
[deferredValue, orgUsers],
18+
);
19+
20+
return (
21+
<div className="flex flex-col gap-2 p-2">
22+
<SelectWithCombobox.Root
23+
open
24+
onSearchValueChange={setSearchValue}
25+
selectedValue={creator}
26+
onSelectedValueChange={setCreator}
27+
>
28+
<SelectWithCombobox.Combobox render={<Input />} autoSelect autoFocus />
29+
<SelectWithCombobox.ComboboxList className="max-h-40">
30+
{matches.map((user) => {
31+
return (
32+
<SelectWithCombobox.ComboboxItem
33+
key={user.userId}
34+
value={user.userId}
35+
className="align-baseline"
36+
>
37+
<div className="flex flex-row items-center gap-4">
38+
<Avatar
39+
firstName={user.firstName}
40+
lastName={user.lastName}
41+
size="m"
42+
/>
43+
<span className="text-grey-100 text-s">
44+
{user.firstName} {user.lastName}
45+
</span>
46+
</div>
47+
</SelectWithCombobox.ComboboxItem>
48+
);
49+
})}
50+
</SelectWithCombobox.ComboboxList>
51+
</SelectWithCombobox.Root>
52+
</div>
53+
);
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { match } from 'ts-pattern';
2+
3+
import { type TestRunFilterName } from '../filters';
4+
import { CreatorsFilter } from './CreatorsFilter';
5+
import { StartedAfterFilter } from './StartedAfterFilter';
6+
import { StatusesFilter } from './StatusesFilter';
7+
import { VersionsFilter } from './VersionsFilter';
8+
9+
export function FilterDetail({
10+
filterName,
11+
}: {
12+
filterName: TestRunFilterName;
13+
}) {
14+
return match(filterName)
15+
.with('startedAfter', () => <StartedAfterFilter />)
16+
.with('statuses', () => <StatusesFilter />)
17+
.with('creators', () => <CreatorsFilter />)
18+
.with('ref_versions', () => <VersionsFilter type="ref" />)
19+
.with('test_versions', () => <VersionsFilter type="test" />)
20+
.exhaustive();
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { getDateFnsLocale } from '@app-builder/services/i18n/i18n-config';
2+
import { useFormatLanguage } from '@app-builder/utils/format';
3+
import { Calendar } from 'ui-design-system';
4+
5+
import { useStartedAfterFilter } from '../TestRunsFiltersContext';
6+
7+
export function StartedAfterFilter() {
8+
const { startedAfter, setStartedAfter } = useStartedAfterFilter();
9+
const language = useFormatLanguage();
10+
11+
return (
12+
<div className="p-4">
13+
<Calendar
14+
mode="single"
15+
selected={startedAfter}
16+
onSelect={setStartedAfter}
17+
locale={getDateFnsLocale(language)}
18+
/>
19+
</div>
20+
);
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { testRunStatuses as statuses } from '@app-builder/models/testrun';
2+
import { matchSorter } from '@app-builder/utils/search';
3+
import { useDeferredValue, useMemo, useState } from 'react';
4+
import { Input, SelectWithCombobox } from 'ui-design-system';
5+
6+
import { TestRunStatus } from '../../TestRunStatus';
7+
import { useStatusesFilter } from '../TestRunsFiltersContext';
8+
9+
export function StatusesFilter() {
10+
const [value, setSearchValue] = useState('');
11+
const { selectedStatuses, setSelectedStatuses } = useStatusesFilter();
12+
const deferredValue = useDeferredValue(value);
13+
14+
const matches = useMemo(
15+
() => matchSorter(statuses, deferredValue),
16+
[deferredValue],
17+
);
18+
19+
return (
20+
<div className="flex flex-col gap-2 p-2">
21+
<SelectWithCombobox.Root
22+
open
23+
onSearchValueChange={setSearchValue}
24+
selectedValue={selectedStatuses}
25+
onSelectedValueChange={setSelectedStatuses}
26+
>
27+
<SelectWithCombobox.Combobox render={<Input />} autoSelect autoFocus />
28+
<SelectWithCombobox.ComboboxList className="max-h-40">
29+
{matches.map((status) => {
30+
return (
31+
<SelectWithCombobox.ComboboxItem
32+
key={status}
33+
value={status}
34+
className="align-baseline"
35+
>
36+
<TestRunStatus status={status} />
37+
</SelectWithCombobox.ComboboxItem>
38+
);
39+
})}
40+
</SelectWithCombobox.ComboboxList>
41+
</SelectWithCombobox.Root>
42+
</div>
43+
);
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { useScenarioIterations } from '@app-builder/routes/_builder+/scenarios+/$scenarioId+/_layout';
2+
import { matchSorter } from '@app-builder/utils/search';
3+
import { useDeferredValue, useMemo, useState } from 'react';
4+
import { Input, SelectWithCombobox } from 'ui-design-system';
5+
6+
import {
7+
useRefVersionFilter,
8+
useTestVersionFilter,
9+
} from '../TestRunsFiltersContext';
10+
11+
export function VersionsFilter({ type }: { type: 'ref' | 'test' }) {
12+
const [value, setSearchValue] = useState('');
13+
const { refVersion, setRefVersion } = useRefVersionFilter();
14+
const { testVersion, setTestVersion } = useTestVersionFilter();
15+
const deferredValue = useDeferredValue(value);
16+
const iterations = useScenarioIterations();
17+
18+
const filteredIterations = useMemo(
19+
() => iterations.filter(({ type }) => type !== 'draft'),
20+
[iterations],
21+
);
22+
23+
const matches = useMemo(
24+
() => matchSorter(filteredIterations, deferredValue, { keys: ['version'] }),
25+
[deferredValue, filteredIterations],
26+
);
27+
28+
return (
29+
<div className="flex flex-col gap-2 p-2">
30+
<SelectWithCombobox.Root
31+
open
32+
onSearchValueChange={setSearchValue}
33+
selectedValue={type === 'ref' ? refVersion : testVersion}
34+
onSelectedValueChange={type === 'ref' ? setRefVersion : setTestVersion}
35+
>
36+
<SelectWithCombobox.Combobox render={<Input />} autoSelect autoFocus />
37+
<SelectWithCombobox.ComboboxList className="max-h-40">
38+
{matches.map((iteration) => {
39+
return (
40+
<SelectWithCombobox.ComboboxItem
41+
key={iteration.id}
42+
value={iteration.id}
43+
className="align-baseline"
44+
>
45+
<span className="text-grey-100 text-s">
46+
{`V${iteration.version}`}
47+
</span>
48+
</SelectWithCombobox.ComboboxItem>
49+
);
50+
})}
51+
</SelectWithCombobox.ComboboxList>
52+
</SelectWithCombobox.Root>
53+
</div>
54+
);
55+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './FilterDetail';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import {
2+
AddNewFilterButton,
3+
ClearAllFiltersButton,
4+
FilterItem,
5+
FilterPopover,
6+
} from '@app-builder/components/Filters';
7+
import { useCallback } from 'react';
8+
import { useTranslation } from 'react-i18next';
9+
import { Separator } from 'ui-design-system';
10+
import { Icon } from 'ui-icons';
11+
12+
import { FilterDetail } from './FilterDetail';
13+
import { getFilterIcon, getFilterTKey } from './filters';
14+
import {
15+
useClearAllFilters,
16+
useClearFilter,
17+
useTestRunsFiltersContext,
18+
useTestRunsFiltersPartition,
19+
} from './TestRunsFiltersContext';
20+
import { TestRunsFiltersMenu } from './TestRunsFiltersMenu';
21+
22+
export function TestRunsFiltersBar() {
23+
const { t } = useTranslation(['scenarios', 'common']);
24+
const { onTestRunsFilterClose } = useTestRunsFiltersContext();
25+
26+
const onOpenChange = useCallback(
27+
(open: boolean) => {
28+
if (!open) {
29+
onTestRunsFilterClose();
30+
}
31+
},
32+
[onTestRunsFilterClose],
33+
);
34+
35+
const { undefinedTestRunsFilterNames, definedTestRunsFilterNames } =
36+
useTestRunsFiltersPartition();
37+
38+
const clearFilter = useClearFilter();
39+
const clearAllFilters = useClearAllFilters();
40+
41+
if (definedTestRunsFilterNames.length === 0) {
42+
return null;
43+
}
44+
45+
return (
46+
<>
47+
<Separator className="bg-grey-10" decorative />
48+
<div className="flex flex-row items-center justify-between gap-2">
49+
<div className="flex flex-row flex-wrap gap-2">
50+
{definedTestRunsFilterNames.map((filterName) => {
51+
const icon = getFilterIcon(filterName);
52+
const tKey = getFilterTKey(filterName);
53+
54+
return (
55+
<FilterPopover.Root key={filterName} onOpenChange={onOpenChange}>
56+
<FilterItem.Root>
57+
<FilterItem.Trigger>
58+
<Icon icon={icon} className="size-5" />
59+
<span className="text-s font-semibold first-letter:capitalize">
60+
<span>{t(tKey)}</span>
61+
</span>
62+
</FilterItem.Trigger>
63+
<FilterItem.Clear
64+
onClick={() => {
65+
clearFilter(filterName);
66+
}}
67+
/>
68+
</FilterItem.Root>
69+
<FilterPopover.Content>
70+
<FilterDetail filterName={filterName} />
71+
</FilterPopover.Content>
72+
</FilterPopover.Root>
73+
);
74+
})}
75+
{undefinedTestRunsFilterNames.length > 0 ? (
76+
<TestRunsFiltersMenu filterNames={undefinedTestRunsFilterNames}>
77+
<AddNewFilterButton />
78+
</TestRunsFiltersMenu>
79+
) : null}
80+
</div>
81+
<ClearAllFiltersButton onClick={clearAllFilters} />
82+
</div>
83+
</>
84+
);
85+
}

0 commit comments

Comments
 (0)