Skip to content

Commit 88ec214

Browse files
feat(Nodes): add grouping (#1584)
1 parent 949a518 commit 88ec214

File tree

25 files changed

+614
-231
lines changed

25 files changed

+614
-231
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@
7676
"typecheck": "tsc --noEmit",
7777
"prepare": "husky",
7878
"test:e2e:install": "npx playwright install --with-deps",
79-
"test:e2e": "npx playwright test --config=playwright.config.ts"
79+
"test:e2e": "npx playwright test --config=playwright.config.ts",
80+
"test:e2e:local": "PLAYWRIGHT_BASE_URL=http://localhost:3000/ npm run test:e2e"
8081
},
8182
"lint-staged": {
8283
"*.{scss, css}": [

src/components/PaginatedTable/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,4 @@ export type RenderControls = (params: ControlsParams) => React.ReactNode;
6767
export type RenderEmptyDataMessage = () => React.ReactNode;
6868
export type RenderErrorMessage = (error: IResponseError) => React.ReactNode;
6969

70-
export type GetRowClassName<T> = (row: T) => string | undefined;
70+
export type GetRowClassName<T> = (row: T) => string;

src/components/Search/Search.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const b = cn('ydb-search');
1111
interface SearchProps {
1212
onChange: (value: string) => void;
1313
value?: string;
14+
width?: React.CSSProperties['width'];
1415
className?: string;
1516
debounce?: number;
1617
placeholder?: string;
@@ -19,6 +20,7 @@ interface SearchProps {
1920
export const Search = ({
2021
onChange,
2122
value = '',
23+
width,
2224
className,
2325
debounce = 200,
2426
placeholder,
@@ -50,6 +52,7 @@ export const Search = ({
5052
<TextInput
5153
hasClear
5254
autoFocus
55+
style={{width}}
5356
className={b(null, className)}
5457
placeholder={placeholder}
5558
value={searchValue}

src/components/nodesColumns/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ export const NODES_COLUMNS_WIDTH_LS_KEY = 'nodesTableColumnsWidth';
77

88
export const NODES_COLUMNS_IDS = {
99
NodeId: 'NodeId',
10+
SystemState: 'SystemState',
1011
Host: 'Host',
12+
Database: 'Database',
1113
NodeName: 'NodeName',
1214
DC: 'DC',
1315
Rack: 'Rack',
@@ -35,9 +37,15 @@ export const NODES_COLUMNS_TITLES = {
3537
get NodeId() {
3638
return i18n('node-id');
3739
},
40+
get SystemState() {
41+
return i18n('system-state');
42+
},
3843
get Host() {
3944
return i18n('host');
4045
},
46+
get Database() {
47+
return i18n('database');
48+
},
4149
get NodeName() {
4250
return i18n('node-name');
4351
},
@@ -95,7 +103,9 @@ export const NODES_COLUMNS_TITLES = {
95103
// Also for some columns we may use more than one field
96104
export const NODES_COLUMNS_TO_DATA_FIELDS: Record<NodesColumnId, NodesRequiredField[]> = {
97105
NodeId: ['NodeId'],
106+
SystemState: ['SystemState'],
98107
Host: ['Host', 'Rack', 'Database', 'SystemState'],
108+
Database: ['Database'],
99109
NodeName: ['NodeName'],
100110
DC: ['DC'],
101111
Rack: ['Rack'],

src/components/nodesColumns/i18n/en.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"node-id": "Node ID",
3+
"system-state": "System State",
34
"host": "Host",
5+
"database": "Database",
46
"node-name": "Node Name",
57
"dc": "DC",
68
"rack": "Rack",

src/containers/Nodes/Nodes.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@
1515
&__node_unavailable {
1616
opacity: 0.6;
1717
}
18+
19+
&__groups-wrapper {
20+
padding-right: 20px;
21+
}
1822
}

src/containers/Nodes/Nodes.tsx

Lines changed: 17 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,41 @@
11
import React from 'react';
22

33
import {ASCENDING} from '@gravity-ui/react-data-table/build/esm/lib/constants';
4-
import {TableColumnSetup} from '@gravity-ui/uikit';
5-
import {StringParam, useQueryParams} from 'use-query-params';
64

7-
import {EntitiesCount} from '../../components/EntitiesCount';
85
import {AccessDenied} from '../../components/Errors/403';
96
import {isAccessError} from '../../components/Errors/PageError/PageError';
107
import {ResponseError} from '../../components/Errors/ResponseError';
118
import {Illustration} from '../../components/Illustration';
12-
import {ProblemFilter} from '../../components/ProblemFilter';
139
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
14-
import {Search} from '../../components/Search';
1510
import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout';
16-
import {UptimeFilter} from '../../components/UptimeFIlter';
1711
import {NODES_COLUMNS_WIDTH_LS_KEY} from '../../components/nodesColumns/constants';
1812
import {nodesApi} from '../../store/reducers/nodes/nodes';
1913
import {filterNodes} from '../../store/reducers/nodes/selectors';
2014
import type {NodesSortParams} from '../../store/reducers/nodes/types';
21-
import {
22-
ProblemFilterValues,
23-
changeFilter,
24-
selectProblemFilter,
25-
} from '../../store/reducers/settings/settings';
26-
import type {ProblemFilterValue} from '../../store/reducers/settings/types';
15+
import {useProblemFilter} from '../../store/reducers/settings/hooks';
2716
import type {AdditionalNodesProps} from '../../types/additionalProps';
28-
import {cn} from '../../utils/cn';
2917
import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
30-
import {
31-
useAutoRefreshInterval,
32-
useTableSort,
33-
useTypedDispatch,
34-
useTypedSelector,
35-
} from '../../utils/hooks';
36-
import {
37-
NodesUptimeFilterValues,
38-
isUnavailableNode,
39-
nodesUptimeFilterValuesSchema,
40-
} from '../../utils/nodes';
18+
import {useAutoRefreshInterval, useTableSort} from '../../utils/hooks';
19+
import {NodesUptimeFilterValues} from '../../utils/nodes';
4120

21+
import {NodesControls} from './NodesControls/NodesControls';
4222
import {useNodesSelectedColumns} from './columns/hooks';
4323
import i18n from './i18n';
24+
import {getRowClassName} from './shared';
25+
import {useNodesPageQueryParams} from './useNodesPageQueryParams';
4426

4527
import './Nodes.scss';
4628

47-
const b = cn('ydb-nodes');
48-
4929
interface NodesProps {
5030
path?: string;
5131
database?: string;
5232
additionalNodesProps?: AdditionalNodesProps;
5333
}
5434

5535
export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) => {
56-
const [queryParams, setQueryParams] = useQueryParams({
57-
uptimeFilter: StringParam,
58-
search: StringParam,
59-
});
60-
const uptimeFilter = nodesUptimeFilterValuesSchema.parse(queryParams.uptimeFilter);
61-
const searchValue = queryParams.search ?? '';
62-
63-
const dispatch = useTypedDispatch();
36+
const {searchValue, uptimeFilter} = useNodesPageQueryParams();
37+
const {problemFilter} = useProblemFilter();
6438

65-
const problemFilter = useTypedSelector(selectProblemFilter);
6639
const [autoRefreshInterval] = useAutoRefreshInterval();
6740

6841
const {columnsToShow, columnsToSelect, setColumns} = useNodesSelectedColumns({
@@ -84,18 +57,6 @@ export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) =
8457
setSortValue(sortParams as NodesSortParams);
8558
});
8659

87-
const handleSearchQueryChange = (value: string) => {
88-
setQueryParams({search: value || undefined}, 'replaceIn');
89-
};
90-
91-
const handleProblemFilterChange = (value: ProblemFilterValue) => {
92-
dispatch(changeFilter(value));
93-
};
94-
95-
const handleUptimeFilterChange = (value: NodesUptimeFilterValues) => {
96-
setQueryParams({uptimeFilter: value}, 'replaceIn');
97-
};
98-
9960
const nodes = React.useMemo(() => {
10061
return filterNodes(data?.Nodes, {searchValue, uptimeFilter, problemFilter});
10162
}, [data, searchValue, uptimeFilter, problemFilter]);
@@ -104,38 +65,19 @@ export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) =
10465

10566
const renderControls = () => {
10667
return (
107-
<React.Fragment>
108-
<Search
109-
onChange={handleSearchQueryChange}
110-
placeholder="Host name"
111-
className={b('search')}
112-
value={searchValue}
113-
/>
114-
<ProblemFilter value={problemFilter} onChange={handleProblemFilterChange} />
115-
<UptimeFilter value={uptimeFilter} onChange={handleUptimeFilterChange} />
116-
<TableColumnSetup
117-
popupWidth={200}
118-
items={columnsToSelect}
119-
showStatus
120-
onUpdate={setColumns}
121-
sortable={false}
122-
/>
123-
<EntitiesCount
124-
total={totalNodes}
125-
current={nodes.length}
126-
label={'Nodes'}
127-
loading={isLoading}
128-
/>
129-
</React.Fragment>
68+
<NodesControls
69+
columnsToSelect={columnsToSelect}
70+
handleSelectedColumnsUpdate={setColumns}
71+
entitiesCountCurrent={nodes.length}
72+
entitiesCountTotal={totalNodes}
73+
entitiesLoading={isLoading}
74+
/>
13075
);
13176
};
13277

13378
const renderTable = () => {
13479
if (nodes.length === 0) {
135-
if (
136-
problemFilter !== ProblemFilterValues.ALL ||
137-
uptimeFilter !== NodesUptimeFilterValues.All
138-
) {
80+
if (problemFilter !== 'All' || uptimeFilter !== NodesUptimeFilterValues.All) {
13981
return <Illustration name="thumbsUp" width="200" />;
14082
}
14183
}
@@ -149,7 +91,7 @@ export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) =
14991
sortOrder={sort}
15092
onSort={handleSort}
15193
emptyDataMessage={i18n('empty.default')}
152-
rowClassName={(row) => b('node', {unavailable: isUnavailableNode(row)})}
94+
rowClassName={getRowClassName}
15395
/>
15496
);
15597
};
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React from 'react';
2+
3+
import type {TableColumnSetupItem} from '@gravity-ui/uikit';
4+
import {Select, TableColumnSetup, Text} from '@gravity-ui/uikit';
5+
6+
import {EntitiesCount} from '../../../components/EntitiesCount';
7+
import {ProblemFilter} from '../../../components/ProblemFilter';
8+
import {Search} from '../../../components/Search';
9+
import {UptimeFilter} from '../../../components/UptimeFIlter';
10+
import {useViewerNodesHandlerHasGroupingBySystemState} from '../../../store/reducers/capabilities/hooks';
11+
import {useProblemFilter} from '../../../store/reducers/settings/hooks';
12+
import {getNodesGroupByOptions} from '../columns/constants';
13+
import i18n from '../i18n';
14+
import {b} from '../shared';
15+
import {useNodesPageQueryParams} from '../useNodesPageQueryParams';
16+
17+
interface NodesControlsProps {
18+
withGroupBySelect?: boolean;
19+
20+
columnsToSelect: TableColumnSetupItem[];
21+
handleSelectedColumnsUpdate: (updated: TableColumnSetupItem[]) => void;
22+
23+
entitiesCountCurrent: number;
24+
entitiesCountTotal?: number;
25+
entitiesLoading: boolean;
26+
}
27+
28+
export function NodesControls({
29+
withGroupBySelect,
30+
31+
columnsToSelect,
32+
handleSelectedColumnsUpdate,
33+
34+
entitiesCountCurrent,
35+
entitiesCountTotal,
36+
entitiesLoading,
37+
}: NodesControlsProps) {
38+
const {
39+
searchValue,
40+
uptimeFilter,
41+
groupByParam,
42+
43+
handleSearchQueryChange,
44+
handleUptimeFilterChange,
45+
handleGroupByParamChange,
46+
} = useNodesPageQueryParams();
47+
const {problemFilter, handleProblemFilterChange} = useProblemFilter();
48+
49+
const systemStateGroupingAvailable = useViewerNodesHandlerHasGroupingBySystemState();
50+
const groupByoptions = getNodesGroupByOptions(systemStateGroupingAvailable);
51+
52+
const handleGroupBySelectUpdate = (value: string[]) => {
53+
handleGroupByParamChange(value[0]);
54+
};
55+
56+
return (
57+
<React.Fragment>
58+
<Search
59+
onChange={handleSearchQueryChange}
60+
placeholder={i18n('controls_search-placeholder')}
61+
width={238}
62+
value={searchValue}
63+
/>
64+
{systemStateGroupingAvailable && withGroupBySelect ? null : (
65+
<ProblemFilter value={problemFilter} onChange={handleProblemFilterChange} />
66+
)}
67+
{withGroupBySelect ? null : (
68+
<UptimeFilter value={uptimeFilter} onChange={handleUptimeFilterChange} />
69+
)}
70+
<TableColumnSetup
71+
popupWidth={200}
72+
items={columnsToSelect}
73+
showStatus
74+
onUpdate={handleSelectedColumnsUpdate}
75+
sortable={false}
76+
/>
77+
{withGroupBySelect ? (
78+
<React.Fragment>
79+
<Text variant="body-2">{i18n('controls_group-by-placeholder')}</Text>
80+
<Select
81+
hasClear
82+
placeholder={'-'}
83+
width={150}
84+
defaultValue={groupByParam ? [groupByParam] : undefined}
85+
onUpdate={handleGroupBySelectUpdate}
86+
options={groupByoptions}
87+
className={b('group-by-select')}
88+
popupClassName={b('group-by-popup')}
89+
/>
90+
</React.Fragment>
91+
) : null}
92+
<EntitiesCount
93+
current={entitiesCountCurrent}
94+
total={entitiesCountTotal}
95+
label={i18n('nodes')}
96+
loading={entitiesLoading}
97+
/>
98+
</React.Fragment>
99+
);
100+
}

0 commit comments

Comments
 (0)