Skip to content

Commit 10018ae

Browse files
authored
feat(Node): display tablets as a table (#855)
1 parent 5a789d5 commit 10018ae

File tree

10 files changed

+164
-346
lines changed

10 files changed

+164
-346
lines changed

src/containers/Tablets/Tablets.scss

Lines changed: 0 additions & 35 deletions
This file was deleted.

src/containers/Tablets/Tablets.tsx

Lines changed: 144 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,151 @@
1-
import React from 'react';
2-
3-
import {Select} from '@gravity-ui/uikit';
1+
import {ArrowsRotateRight} from '@gravity-ui/icons';
2+
import type {Column as DataTableColumn} from '@gravity-ui/react-data-table';
3+
import {Icon, Label, Text} from '@gravity-ui/uikit';
44
import {skipToken} from '@reduxjs/toolkit/query';
5-
import ReactList from 'react-list';
65

6+
import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
7+
import {EntityStatus} from '../../components/EntityStatus/EntityStatus';
78
import {ResponseError} from '../../components/Errors/ResponseError';
8-
import {Loader} from '../../components/Loader';
9-
import {Tablet} from '../../components/Tablet';
10-
import TabletsOverall from '../../components/TabletsOverall/TabletsOverall';
11-
import {setStateFilter, setTypeFilter, tabletsApi} from '../../store/reducers/tablets';
12-
import type {ETabletState, EType} from '../../types/api/tablet';
9+
import {InternalLink} from '../../components/InternalLink';
10+
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
11+
import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton';
12+
import routes, {createHref} from '../../routes';
13+
import {selectTabletsWithFqdn, tabletsApi} from '../../store/reducers/tablets';
14+
import {ETabletState} from '../../types/api/tablet';
15+
import type {TTabletStateInfo} from '../../types/api/tablet';
1316
import type {TabletsApiRequestParams} from '../../types/store/tablets';
1417
import {cn} from '../../utils/cn';
18+
import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
19+
import {calcUptime} from '../../utils/dataFormatters/dataFormatters';
1520
import {useTypedDispatch, useTypedSelector} from '../../utils/hooks';
21+
import {mapTabletStateToLabelTheme} from '../../utils/tablet';
22+
import {getDefaultNodePath} from '../Node/NodePages';
1623

1724
import i18n from './i18n';
1825

19-
import './Tablets.scss';
20-
2126
const b = cn('tablets');
2227

28+
const columns: DataTableColumn<TTabletStateInfo & {fqdn?: string}>[] = [
29+
{
30+
name: 'Type',
31+
get header() {
32+
return i18n('Type');
33+
},
34+
render: ({row}) => {
35+
return (
36+
<span>
37+
{row.Type} {row.Leader ? <Text color="secondary">leader</Text> : ''}
38+
</span>
39+
);
40+
},
41+
},
42+
{
43+
name: 'TabletId',
44+
get header() {
45+
return i18n('Tablet');
46+
},
47+
render: ({row}) => {
48+
const tabletPath =
49+
row.TabletId &&
50+
createHref(routes.tablet, {id: row.TabletId}, {nodeId: row.NodeId, type: row.Type});
51+
52+
return <InternalLink to={tabletPath}>{row.TabletId}</InternalLink>;
53+
},
54+
},
55+
{
56+
name: 'State',
57+
get header() {
58+
return i18n('State');
59+
},
60+
render: ({row}) => {
61+
return <Label theme={mapTabletStateToLabelTheme(row.State)}>{row.State}</Label>;
62+
},
63+
},
64+
{
65+
name: 'NodeId',
66+
get header() {
67+
return i18n('Node ID');
68+
},
69+
render: ({row}) => {
70+
const nodePath = row.NodeId === undefined ? undefined : getDefaultNodePath(row.NodeId);
71+
return <InternalLink to={nodePath}>{row.NodeId}</InternalLink>;
72+
},
73+
align: 'right',
74+
},
75+
{
76+
name: 'FQDN',
77+
get header() {
78+
return i18n('Node FQDN');
79+
},
80+
render: ({row}) => {
81+
if (!row.fqdn) {
82+
return <span></span>;
83+
}
84+
return <EntityStatus name={row.fqdn} showStatus={false} hasClipboardButton />;
85+
},
86+
},
87+
{
88+
name: 'Generation',
89+
get header() {
90+
return i18n('Generation');
91+
},
92+
align: 'right',
93+
},
94+
{
95+
name: 'Uptime',
96+
get header() {
97+
return i18n('Uptime');
98+
},
99+
render: ({row}) => {
100+
return calcUptime(row.ChangeTime);
101+
},
102+
sortAccessor: (row) => -Number(row.ChangeTime),
103+
align: 'right',
104+
},
105+
{
106+
name: 'Actions',
107+
sortable: false,
108+
resizeable: false,
109+
header: '',
110+
render: ({row}) => {
111+
return <TabletActions {...row} />;
112+
},
113+
},
114+
];
115+
116+
function TabletActions(tablet: TTabletStateInfo) {
117+
const isDisabledRestart = tablet.State === ETabletState.Stopped;
118+
const dispatch = useTypedDispatch();
119+
return (
120+
<ButtonWithConfirmDialog
121+
buttonView="outlined"
122+
dialogContent={i18n('dialog.kill')}
123+
onConfirmAction={() => {
124+
return window.api.killTablet(tablet.TabletId);
125+
}}
126+
onConfirmActionSuccess={() => {
127+
dispatch(tabletsApi.util.invalidateTags(['All']));
128+
}}
129+
buttonDisabled={isDisabledRestart}
130+
>
131+
<Icon data={ArrowsRotateRight} />
132+
</ButtonWithConfirmDialog>
133+
);
134+
}
135+
23136
interface TabletsProps {
24137
path?: string;
25138
nodeId?: string | number;
26139
className?: string;
27140
}
28141

29-
export const Tablets = ({path, nodeId, className}: TabletsProps) => {
30-
const dispatch = useTypedDispatch();
31-
32-
const {stateFilter, typeFilter} = useTypedSelector((state) => state.tablets);
142+
export function Tablets({nodeId, path, className}: TabletsProps) {
33143
const {autorefresh} = useTypedSelector((state) => state.schema);
34144

35145
let params: TabletsApiRequestParams | typeof skipToken = skipToken;
36-
if (nodeId) {
37-
params = {nodes: [String(nodeId)]};
146+
const node = nodeId === undefined ? undefined : String(nodeId);
147+
if (node !== undefined) {
148+
params = {nodes: [String(node)]};
38149
} else if (path) {
39150
params = {path};
40151
}
@@ -43,94 +154,23 @@ export const Tablets = ({path, nodeId, className}: TabletsProps) => {
43154
});
44155

45156
const loading = isFetching && currentData === undefined;
46-
const tablets = React.useMemo(() => currentData?.TabletStateInfo || [], [currentData]);
47-
48-
const tabletsToRender = React.useMemo(() => {
49-
let filteredTablets = tablets;
50-
51-
if (typeFilter.length > 0) {
52-
filteredTablets = filteredTablets.filter((tablet) =>
53-
typeFilter.some((filter) => tablet.Type === filter),
54-
);
55-
}
56-
if (stateFilter.length > 0) {
57-
filteredTablets = filteredTablets.filter((tablet) =>
58-
stateFilter.some((filter) => tablet.State === filter),
59-
);
60-
}
61-
return filteredTablets;
62-
}, [tablets, stateFilter, typeFilter]);
63-
64-
const handleStateFilterChange = (value: string[]) => {
65-
dispatch(setStateFilter(value as ETabletState[]));
66-
};
67-
68-
const handleTypeFilterChange = (value: string[]) => {
69-
dispatch(setTypeFilter(value as EType[]));
70-
};
71-
72-
const renderTablet = (tabletIndex: number) => {
73-
return <Tablet tablet={tabletsToRender[tabletIndex]} key={tabletIndex} />;
74-
};
75-
76-
const renderContent = () => {
77-
const states = Array.from(new Set(tablets.map((tablet) => tablet.State)))
78-
.filter((state): state is ETabletState => state !== undefined)
79-
.map((item) => ({
80-
value: item,
81-
content: item,
82-
}));
83-
const types = Array.from(new Set(tablets.map((tablet) => tablet.Type)))
84-
.filter((type): type is EType => type !== undefined)
85-
.map((item) => ({
86-
value: item,
87-
content: item,
88-
}));
89-
90-
return (
91-
<div className={b(null, className)}>
92-
<div className={b('header')}>
93-
<Select
94-
className={b('filter-control')}
95-
multiple
96-
placeholder={i18n('controls.allItems')}
97-
label={`${i18n('controls.state')}:`}
98-
options={states}
99-
value={stateFilter}
100-
onUpdate={handleStateFilterChange}
101-
/>
102-
<Select
103-
className={b('filter-control')}
104-
multiple
105-
placeholder={i18n('controls.allItems')}
106-
label={`${i18n('controls.type')}:`}
107-
options={types}
108-
value={typeFilter}
109-
onUpdate={handleTypeFilterChange}
110-
/>
111-
<TabletsOverall tablets={tablets} />
112-
</div>
113-
114-
<div className={b('items')}>
115-
<ReactList
116-
itemRenderer={renderTablet}
117-
length={tabletsToRender.length}
118-
type="uniform"
119-
/>
120-
</div>
121-
</div>
122-
);
123-
};
157+
const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, node, path));
124158

125159
if (loading) {
126-
return <Loader />;
127-
} else if (error) {
160+
return <TableSkeleton />;
161+
}
162+
if (error) {
128163
return <ResponseError error={error} />;
129-
} else {
130-
return tablets.length > 0 ? (
131-
renderContent()
132-
) : (
133-
<div className="error">{i18n('noTabletsData')}</div>
134-
);
135164
}
136-
};
165+
166+
return (
167+
<div className={b(null, className)}>
168+
<ResizeableDataTable
169+
columns={columns}
170+
data={tablets}
171+
settings={DEFAULT_TABLE_SETTINGS}
172+
emptyDataMessage={i18n('noTabletsData')}
173+
/>
174+
</div>
175+
);
176+
}
Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
{
2-
"controls.type": "Type",
3-
"controls.state": "State",
4-
"controls.allItems": "All items",
5-
"noTabletsData": "No tablets data"
2+
"noTabletsData": "No tablets data",
3+
"Type": "Type",
4+
"Tablet": "Tablet",
5+
"State": "State",
6+
"Node ID": "Node ID",
7+
"Node FQDN": "Node FQDN",
8+
"Generation": "Generation",
9+
"Uptime": "Uptime",
10+
"dialog.kill": "The tablet will be restarted. Do you want to proceed?"
611
}
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import {registerKeysets} from '../../../utils/i18n';
22

33
import en from './en.json';
4-
import ru from './ru.json';
54

65
const COMPONENT = 'ydb-tablets';
76

8-
export default registerKeysets(COMPONENT, {ru, en});
7+
export default registerKeysets(COMPONENT, {en});

src/containers/Tablets/i18n/ru.json

Lines changed: 0 additions & 6 deletions
This file was deleted.

src/containers/Tenant/Diagnostics/Diagnostics.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks';
1717
import {Heatmap} from '../../Heatmap';
1818
import {NodesWrapper} from '../../Nodes/NodesWrapper';
1919
import {StorageWrapper} from '../../Storage/StorageWrapper';
20+
import {Tablets} from '../../Tablets';
2021
import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer';
2122
import {TenantTabsGroups} from '../TenantPages';
2223
import {isDatabaseEntityType} from '../utils/schema';
@@ -29,7 +30,6 @@ import {DATABASE_PAGES, getPagesByType} from './DiagnosticsPages';
2930
import {HotKeys} from './HotKeys/HotKeys';
3031
import {Network} from './Network/Network';
3132
import {Partitions} from './Partitions/Partitions';
32-
import {Tablets} from './Tablets/Tablets';
3333
import {TopQueries} from './TopQueries';
3434
import {TopShards} from './TopShards';
3535

0 commit comments

Comments
 (0)