Skip to content

Commit 2d2c53a

Browse files
authored
[UI] Implement property filter on Run list page (#2762)
* [Feature] Property filter for runs * Added filtering by username dstackai/dstack-cloud#291
1 parent 0ac47fc commit 2d2c53a

File tree

5 files changed

+148
-79
lines changed

5 files changed

+148
-79
lines changed

frontend/src/components/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export type { ChatBubbleProps } from '@cloudscape-design/chat-components/chat-bu
5252
export { default as Avatar } from '@cloudscape-design/chat-components/avatar';
5353
export type { AvatarProps } from '@cloudscape-design/chat-components/avatar';
5454
export { default as LineChart } from '@cloudscape-design/components/line-chart';
55+
export { default as PropertyFilter } from '@cloudscape-design/components/property-filter';
56+
export type { PropertyFilterProps } from '@cloudscape-design/components/property-filter';
5557
export type { LineChartProps } from '@cloudscape-design/components/line-chart/interfaces';
5658
export type { ModalProps } from '@cloudscape-design/components/modal';
5759
export type { TilesProps } from '@cloudscape-design/components/tiles';
Lines changed: 111 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,136 @@
1-
import { useEffect } from 'react';
1+
import { useMemo, useState } from 'react';
22
import { useSearchParams } from 'react-router-dom';
33

4-
import { SelectCSDProps } from 'components';
4+
import type { PropertyFilterProps } from 'components';
55

6-
import { useLocalStorageState } from 'hooks/useLocalStorageState';
76
import { useProjectFilter } from 'hooks/useProjectFilter';
87

98
type Args = {
109
localStorePrefix: string;
11-
projectSearchKey?: string;
12-
selectedProject?: string;
1310
};
1411

15-
export const useFilters = ({ localStorePrefix, projectSearchKey }: Args) => {
16-
const [searchParams] = useSearchParams();
17-
const { selectedProject, setSelectedProject, projectOptions } = useProjectFilter({ localStorePrefix });
18-
const [onlyActive, setOnlyActive] = useLocalStorageState<boolean>(`${localStorePrefix}-is-active`, false);
12+
type RequestParamsKeys = keyof Pick<TRunsRequestParams, 'only_active' | 'project_name' | 'username'>;
1913

20-
const setSelectedOptionFromParams = (
21-
searchKey: string,
22-
options: SelectCSDProps.Options | null,
23-
set: (option: SelectCSDProps.Option) => void,
24-
) => {
25-
const searchValue = searchParams.get(searchKey);
14+
const FilterKeys: Record<string, RequestParamsKeys> = {
15+
PROJECT_NAME: 'project_name',
16+
USER_NAME: 'username',
17+
ACTIVE: 'only_active',
18+
};
19+
20+
const EMPTY_QUERY: PropertyFilterProps.Query = {
21+
tokens: [],
22+
operation: 'and',
23+
};
2624

27-
if (!searchValue || !options?.length) return;
25+
const tokensToRequestParams = (tokens: PropertyFilterProps.Query['tokens']) => {
26+
return tokens.reduce((acc, token) => {
27+
if (token.propertyKey) {
28+
acc[token.propertyKey as RequestParamsKeys] = token.value;
29+
}
30+
31+
return acc;
32+
}, {} as Record<RequestParamsKeys, string>);
33+
};
34+
35+
export const useFilters = ({ localStorePrefix }: Args) => {
36+
const [searchParams, setSearchParams] = useSearchParams();
37+
const { projectOptions } = useProjectFilter({ localStorePrefix });
38+
39+
const [propertyFilterQuery, setPropertyFilterQuery] = useState<PropertyFilterProps.Query>(() => {
40+
const tokens = [];
2841

2942
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
3043
// @ts-ignore
31-
const selectedOption = options.find((option) => option?.value === searchValue);
44+
for (const [paramKey, paramValue] of searchParams.entries()) {
45+
if (Object.values(FilterKeys).includes(paramKey)) {
46+
tokens.push({ propertyKey: paramKey, operator: '=', value: paramValue });
47+
}
48+
}
49+
50+
if (!tokens.length) {
51+
return EMPTY_QUERY;
52+
}
53+
54+
return {
55+
...EMPTY_QUERY,
56+
tokens,
57+
};
58+
});
3259

33-
if (selectedOption) set(selectedOption);
60+
const clearFilter = () => {
61+
setSearchParams({});
62+
setPropertyFilterQuery(EMPTY_QUERY);
3463
};
3564

36-
useEffect(() => {
37-
if (!projectSearchKey) return;
65+
const filteringOptions = useMemo(() => {
66+
const options: PropertyFilterProps.FilteringOption[] = [];
3867

39-
setSelectedOptionFromParams(projectSearchKey, projectOptions, setSelectedProject);
40-
}, [searchParams, projectSearchKey, projectOptions]);
68+
projectOptions.forEach(({ value }) => {
69+
if (value)
70+
options.push({
71+
propertyKey: FilterKeys.PROJECT_NAME,
72+
value,
73+
});
74+
});
4175

42-
const clearSelected = () => {
43-
setSelectedProject(null);
44-
setOnlyActive(false);
45-
};
76+
options.push({
77+
propertyKey: FilterKeys.ACTIVE,
78+
value: 'True',
79+
});
80+
81+
return options;
82+
}, [projectOptions]);
83+
84+
const filteringProperties = [
85+
{
86+
key: FilterKeys.PROJECT_NAME,
87+
operators: ['='],
88+
propertyLabel: 'Project',
89+
groupValuesLabel: 'Project values',
90+
},
91+
{
92+
key: FilterKeys.USER_NAME,
93+
operators: ['='],
94+
propertyLabel: 'Username',
95+
},
96+
{
97+
key: FilterKeys.ACTIVE,
98+
operators: ['='],
99+
propertyLabel: 'Only active',
100+
groupValuesLabel: 'Active values',
101+
},
102+
];
103+
104+
const onChangePropertyFilter: PropertyFilterProps['onChange'] = ({ detail }) => {
105+
const { tokens, operation } = detail;
46106

47-
const setSelectedProjectHandle = (project: SelectCSDProps.Option | null) => {
48-
setSelectedProject(project);
49-
setOnlyActive(false);
107+
const filteredTokens = tokens.filter((token, tokenIndex) => {
108+
return !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex);
109+
});
110+
111+
setSearchParams(tokensToRequestParams(filteredTokens));
112+
113+
setPropertyFilterQuery({
114+
operation,
115+
tokens: filteredTokens,
116+
});
50117
};
51118

119+
const filteringRequestParams = useMemo(() => {
120+
const params = tokensToRequestParams(propertyFilterQuery.tokens);
121+
122+
return {
123+
...params,
124+
only_active: params.only_active === 'True',
125+
};
126+
}, [propertyFilterQuery]);
127+
52128
return {
53-
projectOptions,
54-
selectedProject,
55-
setSelectedProject: setSelectedProjectHandle,
56-
onlyActive,
57-
setOnlyActive,
58-
clearSelected,
129+
filteringRequestParams,
130+
clearFilter,
131+
propertyFilterQuery,
132+
onChangePropertyFilter,
133+
filteringOptions,
134+
filteringProperties,
59135
} as const;
60136
};

frontend/src/pages/Runs/List/index.tsx

Lines changed: 27 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
import React from 'react';
22
import { useTranslation } from 'react-i18next';
3-
import { useSearchParams } from 'react-router-dom';
43

5-
import { Button, FormField, Header, Loader, SelectCSD, SpaceBetween, Table, Toggle } from 'components';
4+
import { Button, Header, Loader, PropertyFilter, SpaceBetween, Table } from 'components';
65

6+
import { DEFAULT_TABLE_PAGE_SIZE } from 'consts';
77
import { useBreadcrumbs, useCollection, useInfiniteScroll } from 'hooks';
88
import { ROUTES } from 'routes';
9+
import { useLazyGetRunsQuery } from 'services/run';
910

1011
import { useRunListPreferences } from './Preferences/useRunListPreferences';
11-
import { DEFAULT_TABLE_PAGE_SIZE } from '../../../consts';
12-
import { useLazyGetRunsQuery } from '../../../services/run';
1312
import {
1413
useAbortRuns,
1514
useColumnsDefinitions,
@@ -25,7 +24,6 @@ import styles from './styles.module.scss';
2524

2625
export const RunList: React.FC = () => {
2726
const { t } = useTranslation();
28-
const [, setSearchParams] = useSearchParams();
2927
const [preferences] = useRunListPreferences();
3028

3129
useBreadcrumbs([
@@ -35,19 +33,23 @@ export const RunList: React.FC = () => {
3533
},
3634
]);
3735

38-
const { projectOptions, selectedProject, setSelectedProject, onlyActive, setOnlyActive, clearSelected } = useFilters({
39-
projectSearchKey: 'project',
36+
const {
37+
clearFilter,
38+
propertyFilterQuery,
39+
onChangePropertyFilter,
40+
filteringOptions,
41+
filteringProperties,
42+
filteringRequestParams,
43+
} = useFilters({
4044
localStorePrefix: 'administration-run-list-page',
4145
});
4246

4347
const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll<IRun, TRunsRequestParams>({
4448
useLazyQuery: useLazyGetRunsQuery,
45-
args: { project_name: selectedProject?.value, only_active: onlyActive, limit: DEFAULT_TABLE_PAGE_SIZE },
49+
args: { ...filteringRequestParams, limit: DEFAULT_TABLE_PAGE_SIZE },
4650
getPaginationParams: (lastRun) => ({ prev_submitted_at: lastRun.submitted_at }),
4751
});
4852

49-
const isDisabledClearFilter = !selectedProject && !onlyActive;
50-
5153
const { stopRuns, isStopping } = useStopRuns();
5254
const { abortRuns, isAborting } = useAbortRuns();
5355
const {
@@ -57,14 +59,7 @@ export const RunList: React.FC = () => {
5759

5860
const { columns } = useColumnsDefinitions();
5961

60-
const clearFilter = () => {
61-
clearSelected();
62-
setOnlyActive(false);
63-
setSearchParams({});
64-
};
65-
6662
const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({
67-
isDisabledClearFilter,
6863
clearFilter,
6964
});
7065

@@ -150,32 +145,21 @@ export const RunList: React.FC = () => {
150145
}
151146
filter={
152147
<div className={styles.selectFilters}>
153-
<div className={styles.select}>
154-
<FormField label={t('projects.run.project')}>
155-
<SelectCSD
156-
disabled={!projectOptions?.length}
157-
options={projectOptions}
158-
selectedOption={selectedProject}
159-
onChange={(event) => {
160-
setSelectedProject(event.detail.selectedOption);
161-
}}
162-
placeholder={t('projects.run.project_placeholder')}
163-
expandToViewport={true}
164-
filteringType="auto"
165-
/>
166-
</FormField>
167-
</div>
168-
169-
<div className={styles.activeOnly}>
170-
<Toggle onChange={({ detail }) => setOnlyActive(detail.checked)} checked={onlyActive}>
171-
{t('projects.run.active_only')}
172-
</Toggle>
173-
</div>
174-
175-
<div className={styles.clear}>
176-
<Button formAction="none" onClick={clearFilter} disabled={isDisabledClearFilter}>
177-
{t('common.clearFilter')}
178-
</Button>
148+
<div className={styles.propertyFilter}>
149+
<PropertyFilter
150+
query={propertyFilterQuery}
151+
onChange={onChangePropertyFilter}
152+
expandToViewport
153+
hideOperations
154+
i18nStrings={{
155+
clearFiltersText: 'Clear filter',
156+
filteringAriaLabel: 'Find runs',
157+
filteringPlaceholder: 'Find runs',
158+
operationAndText: 'and',
159+
}}
160+
filteringOptions={filteringOptions}
161+
filteringProperties={filteringProperties}
162+
/>
179163
</div>
180164
</div>
181165
}

frontend/src/pages/Runs/List/styles.module.scss

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,13 @@
88
width: var(--select-width, 30%);
99
}
1010

11+
.propertyFilter {
12+
//width: 400px;
13+
//max-width: 100%;
14+
flex-grow: 1;
15+
min-width: 0;
16+
}
17+
1118
.activeOnly {
1219
display: flex;
1320
align-items: center;

frontend/src/types/run.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
declare type TRunsRequestParams = {
22
project_name?: IProject['project_name'];
33
repo_id?: string;
4-
user_name?: string;
4+
username?: string;
55
only_active?: boolean;
66
prev_submitted_at?: string;
77
prev_run_id?: string;

0 commit comments

Comments
 (0)