Skip to content

Commit bdb9aab

Browse files
authored
[Feature]: Property filter on Fleets, Models, Volumes pages #2817 (#2824)
1 parent 48d19d8 commit bdb9aab

12 files changed

Lines changed: 416 additions & 231 deletions

File tree

frontend/src/libs/filters.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import type { PropertyFilterProps } from 'components';
2+
3+
export const tokensToRequestParams = <RequestParamsKeys extends string>(
4+
tokens: PropertyFilterProps.Query['tokens'],
5+
onlyActive?: boolean,
6+
) => {
7+
const params: Record<RequestParamsKeys | 'only_active', string> = tokens.reduce((acc, token) => {
8+
if (token.propertyKey) {
9+
acc[token.propertyKey as RequestParamsKeys] = token.value;
10+
}
11+
12+
return acc;
13+
}, {} as Record<RequestParamsKeys | 'only_active', string>);
14+
15+
if (onlyActive) {
16+
params['only_active'] = 'true';
17+
}
18+
19+
return params;
20+
};
21+
22+
export const EMPTY_QUERY: PropertyFilterProps.Query = {
23+
tokens: [],
24+
operation: 'and',
25+
};
26+
27+
export const requestParamsToTokens = <RequestParamsKeys extends string>({
28+
searchParams,
29+
filterKeys,
30+
}: {
31+
searchParams: URLSearchParams;
32+
filterKeys: Record<string, RequestParamsKeys>;
33+
}): PropertyFilterProps.Query => {
34+
const tokens = [];
35+
36+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
37+
// @ts-ignore
38+
for (const [paramKey, paramValue] of searchParams.entries()) {
39+
if (Object.values(filterKeys).includes(paramKey)) {
40+
tokens.push({ propertyKey: paramKey, operator: '=', value: paramValue });
41+
}
42+
}
43+
44+
if (!tokens.length) {
45+
return EMPTY_QUERY;
46+
}
47+
48+
return {
49+
...EMPTY_QUERY,
50+
tokens,
51+
};
52+
};

frontend/src/locale/en.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,7 @@
448448
"nomatch_message_text": "We can't find a match.",
449449
"nomatch_message_button_label": "Clear filter",
450450
"active_only": "Active fleets",
451+
"filter_property_placeholder": "Filter fleets by properties",
451452
"statuses": {
452453
"active": "Active",
453454
"submitted": "Submitted",
@@ -457,6 +458,7 @@
457458
},
458459
"instances": {
459460
"active_only": "Active instances",
461+
"filter_property_placeholder": "Filter instances by properties",
460462
"title": "Instances",
461463
"empty_message_title": "No instances",
462464
"empty_message_text": "No instances to display.",
@@ -494,6 +496,7 @@
494496
"delete_volumes_confirm_title": "Delete volumes",
495497
"delete_volumes_confirm_message": "Are you sure you want to delete these volumes?",
496498
"active_only": "Active volumes",
499+
"filter_property_placeholder": "Filter volumes by properties",
497500

498501
"name": "Name",
499502
"project": "Project name",

frontend/src/pages/Fleets/List/hooks.tsx

Lines changed: 88 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,46 @@
1-
import React, { useCallback } from 'react';
1+
import React, { useCallback, useMemo, useState } from 'react';
22
import { useTranslation } from 'react-i18next';
3+
import { useSearchParams } from 'react-router-dom';
34
import { format } from 'date-fns';
5+
import { ToggleProps } from '@cloudscape-design/components';
46

7+
import type { PropertyFilterProps } from 'components';
58
import { Button, ListEmptyMessage, NavigateLink, StatusIndicator, TableProps } from 'components';
69

710
import { DATE_TIME_FORMAT } from 'consts';
8-
import { useLocalStorageState } from 'hooks/useLocalStorageState';
911
import { useProjectFilter } from 'hooks/useProjectFilter';
12+
import { EMPTY_QUERY, requestParamsToTokens, tokensToRequestParams } from 'libs/filters';
1013
import { getFleetInstancesLinkText, getFleetPrice, getFleetStatusIconType } from 'libs/fleet';
1114
import { ROUTES } from 'routes';
1215

1316
export const useEmptyMessages = ({
14-
clearFilters,
17+
clearFilter,
1518
isDisabledClearFilter,
1619
}: {
17-
clearFilters?: () => void;
20+
clearFilter?: () => void;
1821
isDisabledClearFilter?: boolean;
1922
}) => {
2023
const { t } = useTranslation();
2124

2225
const renderEmptyMessage = useCallback<() => React.ReactNode>(() => {
2326
return (
2427
<ListEmptyMessage title={t('fleets.empty_message_title')} message={t('fleets.empty_message_text')}>
25-
<Button disabled={isDisabledClearFilter} onClick={clearFilters}>
28+
<Button disabled={isDisabledClearFilter} onClick={clearFilter}>
2629
{t('common.clearFilter')}
2730
</Button>
2831
</ListEmptyMessage>
2932
);
30-
}, [clearFilters, isDisabledClearFilter]);
33+
}, [clearFilter, isDisabledClearFilter]);
3134

3235
const renderNoMatchMessage = useCallback<() => React.ReactNode>(() => {
3336
return (
3437
<ListEmptyMessage title={t('fleets.nomatch_message_title')} message={t('fleets.nomatch_message_text')}>
35-
<Button disabled={isDisabledClearFilter} onClick={clearFilters}>
38+
<Button disabled={isDisabledClearFilter} onClick={clearFilter}>
3639
{t('common.clearFilter')}
3740
</Button>
3841
</ListEmptyMessage>
3942
);
40-
}, [clearFilters, isDisabledClearFilter]);
43+
}, [clearFilter, isDisabledClearFilter]);
4144

4245
return { renderEmptyMessage, renderNoMatchMessage } as const;
4346
};
@@ -99,24 +102,91 @@ export const useColumnsDefinitions = () => {
99102
return { columns } as const;
100103
};
101104

105+
type RequestParamsKeys = keyof Pick<TFleetListRequestParams, 'only_active' | 'project_name'>;
106+
107+
const filterKeys: Record<string, RequestParamsKeys> = {
108+
PROJECT_NAME: 'project_name',
109+
};
110+
102111
export const useFilters = (localStorePrefix = 'fleet-list-page') => {
103-
const [onlyActive, setOnlyActive] = useLocalStorageState<boolean>(`${localStorePrefix}-is-active`, true);
104-
const { selectedProject, setSelectedProject, projectOptions } = useProjectFilter({ localStorePrefix });
112+
const [searchParams, setSearchParams] = useSearchParams();
113+
const [onlyActive, setOnlyActive] = useState(() => searchParams.get('only_active') === 'true');
114+
const { projectOptions } = useProjectFilter({ localStorePrefix });
105115

106-
const clearFilters = () => {
116+
const [propertyFilterQuery, setPropertyFilterQuery] = useState<PropertyFilterProps.Query>(() =>
117+
requestParamsToTokens<RequestParamsKeys>({ searchParams, filterKeys }),
118+
);
119+
120+
const clearFilter = () => {
121+
setSearchParams({});
107122
setOnlyActive(false);
108-
setSelectedProject(null);
123+
setPropertyFilterQuery(EMPTY_QUERY);
109124
};
110125

111-
const isDisabledClearFilter = !selectedProject && !onlyActive;
126+
const filteringOptions = useMemo(() => {
127+
const options: PropertyFilterProps.FilteringOption[] = [];
128+
129+
projectOptions.forEach(({ value }) => {
130+
if (value)
131+
options.push({
132+
propertyKey: filterKeys.PROJECT_NAME,
133+
value,
134+
});
135+
});
136+
137+
return options;
138+
}, [projectOptions]);
139+
140+
const filteringProperties = [
141+
{
142+
key: filterKeys.PROJECT_NAME,
143+
operators: ['='],
144+
propertyLabel: 'Project',
145+
groupValuesLabel: 'Project values',
146+
},
147+
];
148+
149+
const onChangePropertyFilter: PropertyFilterProps['onChange'] = ({ detail }) => {
150+
const { tokens, operation } = detail;
151+
152+
const filteredTokens = tokens.filter((token, tokenIndex) => {
153+
return !tokens.some((item, index) => token.propertyKey === item.propertyKey && index > tokenIndex);
154+
});
155+
156+
setSearchParams(tokensToRequestParams<RequestParamsKeys>(filteredTokens, onlyActive));
157+
158+
setPropertyFilterQuery({
159+
operation,
160+
tokens: filteredTokens,
161+
});
162+
};
163+
164+
const onChangeOnlyActive: ToggleProps['onChange'] = ({ detail }) => {
165+
setOnlyActive(detail.checked);
166+
167+
setSearchParams(tokensToRequestParams<RequestParamsKeys>(propertyFilterQuery.tokens, detail.checked));
168+
};
169+
170+
const filteringRequestParams = useMemo(() => {
171+
const params = tokensToRequestParams<RequestParamsKeys>(propertyFilterQuery.tokens);
172+
173+
return {
174+
...params,
175+
only_active: onlyActive,
176+
};
177+
}, [propertyFilterQuery, onlyActive]);
178+
179+
const isDisabledClearFilter = !propertyFilterQuery.tokens.length && !onlyActive;
112180

113181
return {
114-
projectOptions,
115-
selectedProject,
116-
setSelectedProject,
182+
filteringRequestParams,
183+
clearFilter,
184+
propertyFilterQuery,
185+
onChangePropertyFilter,
186+
filteringOptions,
187+
filteringProperties,
117188
onlyActive,
118-
setOnlyActive,
119-
clearFilters,
189+
onChangeOnlyActive,
120190
isDisabledClearFilter,
121191
} as const;
122192
};

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

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { useTranslation } from 'react-i18next';
33

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

66
import { DEFAULT_TABLE_PAGE_SIZE } from 'consts';
77
import { useBreadcrumbs, useCollection, useInfiniteScroll } from 'hooks';
@@ -24,18 +24,20 @@ export const FleetList: React.FC = () => {
2424
]);
2525

2626
const {
27+
clearFilter,
28+
propertyFilterQuery,
29+
onChangePropertyFilter,
30+
filteringOptions,
31+
filteringProperties,
32+
filteringRequestParams,
2733
onlyActive,
28-
setOnlyActive,
34+
onChangeOnlyActive,
2935
isDisabledClearFilter,
30-
clearFilters,
31-
projectOptions,
32-
selectedProject,
33-
setSelectedProject,
3436
} = useFilters();
3537

3638
const { data, isLoading, refreshList, isLoadingMore } = useInfiniteScroll<IFleet, TFleetListRequestParams>({
3739
useLazyQuery: useLazyGetFleetsQuery,
38-
args: { project_name: selectedProject?.value, only_active: onlyActive, limit: DEFAULT_TABLE_PAGE_SIZE },
40+
args: { ...filteringRequestParams, limit: DEFAULT_TABLE_PAGE_SIZE },
3941

4042
getPaginationParams: (lastFleet) => ({
4143
prev_created_at: lastFleet.created_at,
@@ -45,7 +47,7 @@ export const FleetList: React.FC = () => {
4547

4648
const { columns } = useColumnsDefinitions();
4749
const { deleteFleets, isDeleting } = useDeleteFleet();
48-
const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({ clearFilters, isDisabledClearFilter });
50+
const { renderEmptyMessage, renderNoMatchMessage } = useEmptyMessages({ clearFilter, isDisabledClearFilter });
4951

5052
const { items, collectionProps } = useCollection<IFleet>(data, {
5153
filtering: {
@@ -98,33 +100,28 @@ export const FleetList: React.FC = () => {
98100
}
99101
filter={
100102
<div className={styles.filters}>
101-
<div className={styles.select}>
102-
<FormField label={t('projects.run.project')}>
103-
<SelectCSD
104-
disabled={!projectOptions?.length}
105-
options={projectOptions}
106-
selectedOption={selectedProject}
107-
onChange={(event) => {
108-
setSelectedProject(event.detail.selectedOption);
109-
}}
110-
placeholder={t('projects.run.project_placeholder')}
111-
expandToViewport={true}
112-
filteringType="auto"
113-
/>
114-
</FormField>
103+
<div className={styles.propertyFilter}>
104+
<PropertyFilter
105+
query={propertyFilterQuery}
106+
onChange={onChangePropertyFilter}
107+
expandToViewport
108+
hideOperations
109+
i18nStrings={{
110+
clearFiltersText: t('common.clearFilter'),
111+
filteringAriaLabel: t('fleets.filter_property_placeholder'),
112+
filteringPlaceholder: t('fleets.filter_property_placeholder'),
113+
operationAndText: 'and',
114+
}}
115+
filteringOptions={filteringOptions}
116+
filteringProperties={filteringProperties}
117+
/>
115118
</div>
116119

117120
<div className={styles.activeOnly}>
118-
<Toggle onChange={({ detail }) => setOnlyActive(detail.checked)} checked={onlyActive}>
121+
<Toggle onChange={onChangeOnlyActive} checked={onlyActive}>
119122
{t('fleets.active_only')}
120123
</Toggle>
121124
</div>
122-
123-
<div className={styles.clear}>
124-
<Button formAction="none" onClick={clearFilters} disabled={isDisabledClearFilter}>
125-
{t('common.clearFilter')}
126-
</Button>
127-
</div>
128125
</div>
129126
}
130127
footer={<Loader show={isLoadingMore} padding={{ vertical: 'm' }} />}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
.filters {
2-
--select-width: calc((688px - 3 * 20px) / 2);
32
display: flex;
43
flex-wrap: wrap;
54
gap: 0 20px;
65

7-
.select {
8-
width: var(--select-width, 30%);
6+
.propertyFilter {
7+
max-width: 640px;
8+
flex-grow: 1;
9+
min-width: 0;
910
}
1011

1112
.activeOnly {
1213
display: flex;
13-
align-items: center;
14-
padding-top: 26px;
14+
padding-top: 7px;
1515
}
1616

1717
.clear {
18-
padding-top: 26px;
18+
1919
}
2020
}

0 commit comments

Comments
 (0)