Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Nodes): add grouping #1584

Merged
merged 3 commits into from
Nov 7, 2024
Merged
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
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@
"typecheck": "tsc --noEmit",
"prepare": "husky",
"test:e2e:install": "npx playwright install --with-deps",
"test:e2e": "npx playwright test --config=playwright.config.ts"
"test:e2e": "npx playwright test --config=playwright.config.ts",
"test:e2e:local": "PLAYWRIGHT_BASE_URL=http://localhost:3000/ npm run test:e2e"
},
"lint-staged": {
"*.{scss, css}": [
Expand Down
2 changes: 1 addition & 1 deletion src/components/PaginatedTable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,4 @@ export type RenderControls = (params: ControlsParams) => React.ReactNode;
export type RenderEmptyDataMessage = () => React.ReactNode;
export type RenderErrorMessage = (error: IResponseError) => React.ReactNode;

export type GetRowClassName<T> = (row: T) => string | undefined;
export type GetRowClassName<T> = (row: T) => string;
3 changes: 3 additions & 0 deletions src/components/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const b = cn('ydb-search');
interface SearchProps {
onChange: (value: string) => void;
value?: string;
width?: React.CSSProperties['width'];
className?: string;
debounce?: number;
placeholder?: string;
Expand All @@ -19,6 +20,7 @@ interface SearchProps {
export const Search = ({
onChange,
value = '',
width,
className,
debounce = 200,
placeholder,
Expand Down Expand Up @@ -50,6 +52,7 @@ export const Search = ({
<TextInput
hasClear
autoFocus
style={{width}}
className={b(null, className)}
placeholder={placeholder}
value={searchValue}
Expand Down
10 changes: 10 additions & 0 deletions src/components/nodesColumns/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ export const NODES_COLUMNS_WIDTH_LS_KEY = 'nodesTableColumnsWidth';

export const NODES_COLUMNS_IDS = {
NodeId: 'NodeId',
SystemState: 'SystemState',
Host: 'Host',
Database: 'Database',
NodeName: 'NodeName',
DC: 'DC',
Rack: 'Rack',
Expand Down Expand Up @@ -35,9 +37,15 @@ export const NODES_COLUMNS_TITLES = {
get NodeId() {
return i18n('node-id');
},
get SystemState() {
return i18n('system-state');
},
get Host() {
return i18n('host');
},
get Database() {
return i18n('database');
},
get NodeName() {
return i18n('node-name');
},
Expand Down Expand Up @@ -95,7 +103,9 @@ export const NODES_COLUMNS_TITLES = {
// Also for some columns we may use more than one field
export const NODES_COLUMNS_TO_DATA_FIELDS: Record<NodesColumnId, NodesRequiredField[]> = {
NodeId: ['NodeId'],
SystemState: ['SystemState'],
Host: ['Host', 'Rack', 'Database', 'SystemState'],
Database: ['Database'],
NodeName: ['NodeName'],
DC: ['DC'],
Rack: ['Rack'],
Expand Down
2 changes: 2 additions & 0 deletions src/components/nodesColumns/i18n/en.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
{
"node-id": "Node ID",
"system-state": "System State",
"host": "Host",
"database": "Database",
"node-name": "Node Name",
"dc": "DC",
"rack": "Rack",
Expand Down
4 changes: 4 additions & 0 deletions src/containers/Nodes/Nodes.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,8 @@
&__node_unavailable {
opacity: 0.6;
}

&__groups-wrapper {
padding-right: 20px;
}
}
92 changes: 17 additions & 75 deletions src/containers/Nodes/Nodes.tsx
Original file line number Diff line number Diff line change
@@ -1,68 +1,41 @@
import React from 'react';

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

import {EntitiesCount} from '../../components/EntitiesCount';
import {AccessDenied} from '../../components/Errors/403';
import {isAccessError} from '../../components/Errors/PageError/PageError';
import {ResponseError} from '../../components/Errors/ResponseError';
import {Illustration} from '../../components/Illustration';
import {ProblemFilter} from '../../components/ProblemFilter';
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
import {Search} from '../../components/Search';
import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout';
import {UptimeFilter} from '../../components/UptimeFIlter';
import {NODES_COLUMNS_WIDTH_LS_KEY} from '../../components/nodesColumns/constants';
import {nodesApi} from '../../store/reducers/nodes/nodes';
import {filterNodes} from '../../store/reducers/nodes/selectors';
import type {NodesSortParams} from '../../store/reducers/nodes/types';
import {
ProblemFilterValues,
changeFilter,
selectProblemFilter,
} from '../../store/reducers/settings/settings';
import type {ProblemFilterValue} from '../../store/reducers/settings/types';
import {useProblemFilter} from '../../store/reducers/settings/hooks';
import type {AdditionalNodesProps} from '../../types/additionalProps';
import {cn} from '../../utils/cn';
import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
import {
useAutoRefreshInterval,
useTableSort,
useTypedDispatch,
useTypedSelector,
} from '../../utils/hooks';
import {
NodesUptimeFilterValues,
isUnavailableNode,
nodesUptimeFilterValuesSchema,
} from '../../utils/nodes';
import {useAutoRefreshInterval, useTableSort} from '../../utils/hooks';
import {NodesUptimeFilterValues} from '../../utils/nodes';

import {NodesControls} from './NodesControls/NodesControls';
import {useNodesSelectedColumns} from './columns/hooks';
import i18n from './i18n';
import {getRowClassName} from './shared';
import {useNodesPageQueryParams} from './useNodesPageQueryParams';

import './Nodes.scss';

const b = cn('ydb-nodes');

interface NodesProps {
path?: string;
database?: string;
additionalNodesProps?: AdditionalNodesProps;
}

export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) => {
const [queryParams, setQueryParams] = useQueryParams({
uptimeFilter: StringParam,
search: StringParam,
});
const uptimeFilter = nodesUptimeFilterValuesSchema.parse(queryParams.uptimeFilter);
const searchValue = queryParams.search ?? '';

const dispatch = useTypedDispatch();
const {searchValue, uptimeFilter} = useNodesPageQueryParams();
const {problemFilter} = useProblemFilter();

const problemFilter = useTypedSelector(selectProblemFilter);
const [autoRefreshInterval] = useAutoRefreshInterval();

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

const handleSearchQueryChange = (value: string) => {
setQueryParams({search: value || undefined}, 'replaceIn');
};

const handleProblemFilterChange = (value: ProblemFilterValue) => {
dispatch(changeFilter(value));
};

const handleUptimeFilterChange = (value: NodesUptimeFilterValues) => {
setQueryParams({uptimeFilter: value}, 'replaceIn');
};

const nodes = React.useMemo(() => {
return filterNodes(data?.Nodes, {searchValue, uptimeFilter, problemFilter});
}, [data, searchValue, uptimeFilter, problemFilter]);
Expand All @@ -104,38 +65,19 @@ export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) =

const renderControls = () => {
return (
<React.Fragment>
<Search
onChange={handleSearchQueryChange}
placeholder="Host name"
className={b('search')}
value={searchValue}
/>
<ProblemFilter value={problemFilter} onChange={handleProblemFilterChange} />
<UptimeFilter value={uptimeFilter} onChange={handleUptimeFilterChange} />
<TableColumnSetup
popupWidth={200}
items={columnsToSelect}
showStatus
onUpdate={setColumns}
sortable={false}
/>
<EntitiesCount
total={totalNodes}
current={nodes.length}
label={'Nodes'}
loading={isLoading}
/>
</React.Fragment>
<NodesControls
columnsToSelect={columnsToSelect}
handleSelectedColumnsUpdate={setColumns}
entitiesCountCurrent={nodes.length}
entitiesCountTotal={totalNodes}
entitiesLoading={isLoading}
/>
);
};

const renderTable = () => {
if (nodes.length === 0) {
if (
problemFilter !== ProblemFilterValues.ALL ||
uptimeFilter !== NodesUptimeFilterValues.All
) {
if (problemFilter !== 'All' || uptimeFilter !== NodesUptimeFilterValues.All) {
return <Illustration name="thumbsUp" width="200" />;
}
}
Expand All @@ -149,7 +91,7 @@ export const Nodes = ({path, database, additionalNodesProps = {}}: NodesProps) =
sortOrder={sort}
onSort={handleSort}
emptyDataMessage={i18n('empty.default')}
rowClassName={(row) => b('node', {unavailable: isUnavailableNode(row)})}
rowClassName={getRowClassName}
/>
);
};
Expand Down
100 changes: 100 additions & 0 deletions src/containers/Nodes/NodesControls/NodesControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import React from 'react';

import type {TableColumnSetupItem} from '@gravity-ui/uikit';
import {Select, TableColumnSetup, Text} from '@gravity-ui/uikit';

import {EntitiesCount} from '../../../components/EntitiesCount';
import {ProblemFilter} from '../../../components/ProblemFilter';
import {Search} from '../../../components/Search';
import {UptimeFilter} from '../../../components/UptimeFIlter';
import {useViewerNodesHandlerHasGroupingBySystemState} from '../../../store/reducers/capabilities/hooks';
import {useProblemFilter} from '../../../store/reducers/settings/hooks';
import {getNodesGroupByOptions} from '../columns/constants';
import i18n from '../i18n';
import {b} from '../shared';
import {useNodesPageQueryParams} from '../useNodesPageQueryParams';

interface NodesControlsProps {
withGroupBySelect?: boolean;

columnsToSelect: TableColumnSetupItem[];
handleSelectedColumnsUpdate: (updated: TableColumnSetupItem[]) => void;

entitiesCountCurrent: number;
entitiesCountTotal?: number;
entitiesLoading: boolean;
}

export function NodesControls({
withGroupBySelect,

columnsToSelect,
handleSelectedColumnsUpdate,

entitiesCountCurrent,
entitiesCountTotal,
entitiesLoading,
}: NodesControlsProps) {
const {
searchValue,
uptimeFilter,
groupByParam,

handleSearchQueryChange,
handleUptimeFilterChange,
handleGroupByParamChange,
} = useNodesPageQueryParams();
const {problemFilter, handleProblemFilterChange} = useProblemFilter();

const systemStateGroupingAvailable = useViewerNodesHandlerHasGroupingBySystemState();
const groupByoptions = getNodesGroupByOptions(systemStateGroupingAvailable);

const handleGroupBySelectUpdate = (value: string[]) => {
handleGroupByParamChange(value[0]);
};

return (
<React.Fragment>
<Search
onChange={handleSearchQueryChange}
placeholder={i18n('controls_search-placeholder')}
width={238}
value={searchValue}
/>
{systemStateGroupingAvailable && withGroupBySelect ? null : (
<ProblemFilter value={problemFilter} onChange={handleProblemFilterChange} />
)}
{withGroupBySelect ? null : (
<UptimeFilter value={uptimeFilter} onChange={handleUptimeFilterChange} />
)}
<TableColumnSetup
popupWidth={200}
items={columnsToSelect}
showStatus
onUpdate={handleSelectedColumnsUpdate}
sortable={false}
/>
{withGroupBySelect ? (
<React.Fragment>
<Text variant="body-2">{i18n('controls_group-by-placeholder')}</Text>
<Select
hasClear
placeholder={'-'}
width={150}
defaultValue={groupByParam ? [groupByParam] : undefined}
onUpdate={handleGroupBySelectUpdate}
options={groupByoptions}
className={b('group-by-select')}
popupClassName={b('group-by-popup')}
/>
</React.Fragment>
) : null}
<EntitiesCount
current={entitiesCountCurrent}
total={entitiesCountTotal}
label={i18n('nodes')}
loading={entitiesLoading}
/>
</React.Fragment>
);
}
Loading
Loading