Skip to content

Commit 7915463

Browse files
authored
feat(Diagnostics): display tablets as table (#852)
1 parent 5aa5b68 commit 7915463

File tree

6 files changed

+240
-3
lines changed

6 files changed

+240
-3
lines changed

src/containers/Tenant/Diagnostics/Diagnostics.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks';
1818
import {Heatmap} from '../../Heatmap';
1919
import {NodesWrapper} from '../../Nodes/NodesWrapper';
2020
import {StorageWrapper} from '../../Storage/StorageWrapper';
21-
import {Tablets} from '../../Tablets';
2221
import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer';
2322
import {TenantTabsGroups} from '../TenantPages';
2423
import {isDatabaseEntityType} from '../utils/schema';
@@ -31,6 +30,7 @@ import {DATABASE_PAGES, getPagesByType} from './DiagnosticsPages';
3130
import {HotKeys} from './HotKeys/HotKeys';
3231
import {Network} from './Network/Network';
3332
import {Partitions} from './Partitions/Partitions';
33+
import {Tablets} from './Tablets/Tablets';
3434
import {TopQueries} from './TopQueries';
3535
import {TopShards} from './TopShards';
3636

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
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';
4+
import {skipToken} from '@reduxjs/toolkit/query';
5+
6+
import {ButtonWithConfirmDialog} from '../../../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
7+
import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus';
8+
import {ResponseError} from '../../../../components/Errors/ResponseError';
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';
16+
import type {TabletsApiRequestParams} from '../../../../types/store/tablets';
17+
import {cn} from '../../../../utils/cn';
18+
import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants';
19+
import {calcUptime} from '../../../../utils/dataFormatters/dataFormatters';
20+
import {useTypedDispatch, useTypedSelector} from '../../../../utils/hooks';
21+
import {mapTabletStateToLabelTheme} from '../../../../utils/tablet';
22+
import {getDefaultNodePath} from '../../../Node/NodePages';
23+
24+
import i18n from './i18n';
25+
26+
const b = cn('tablets-table');
27+
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+
136+
interface TabletsProps {
137+
path?: string;
138+
className?: string;
139+
}
140+
141+
export function Tablets({path, className}: TabletsProps) {
142+
const {autorefresh} = useTypedSelector((state) => state.schema);
143+
144+
let params: TabletsApiRequestParams | typeof skipToken = skipToken;
145+
if (path) {
146+
params = {path};
147+
}
148+
const {currentData, isFetching, error} = tabletsApi.useGetTabletsInfoQuery(params, {
149+
pollingInterval: autorefresh,
150+
});
151+
152+
const loading = isFetching && currentData === undefined;
153+
const tablets = useTypedSelector((state) => selectTabletsWithFqdn(state, path || ''));
154+
155+
if (loading) {
156+
return <TableSkeleton />;
157+
}
158+
if (error) {
159+
return <ResponseError error={error} />;
160+
}
161+
162+
return (
163+
<div className={b(null, className)}>
164+
<ResizeableDataTable
165+
columns={columns}
166+
data={tablets}
167+
settings={DEFAULT_TABLE_SETTINGS}
168+
emptyDataMessage={i18n('noTabletsData')}
169+
/>
170+
</div>
171+
);
172+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
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?"
11+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {registerKeysets} from '../../../../../utils/i18n';
2+
3+
import en from './en.json';
4+
5+
const COMPONENT = 'ydb-tablets-table';
6+
7+
export default registerKeysets(COMPONENT, {en});

src/store/reducers/tablets.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import {createSlice} from '@reduxjs/toolkit';
1+
import {createSelector, createSlice} from '@reduxjs/toolkit';
22
import type {PayloadAction} from '@reduxjs/toolkit';
33

4-
import type {ETabletState, EType} from '../../types/api/tablet';
4+
import type {ETabletState, EType, TTabletStateInfo} from '../../types/api/tablet';
55
import type {TabletsApiRequestParams, TabletsState} from '../../types/store/tablets';
6+
import type {RootState} from '../defaultStore';
67

78
import {api} from './api';
9+
import {selectNodesMap} from './nodesList';
810

911
const initialState: TabletsState = {
1012
stateFilter: [],
@@ -43,3 +45,31 @@ export const tabletsApi = api.injectEndpoints({
4345
}),
4446
overrideExisting: 'throw',
4547
});
48+
49+
const getTabletsInfoSelector = createSelector(
50+
(path: string) => path,
51+
(path) => tabletsApi.endpoints.getTabletsInfo.select({path}),
52+
);
53+
54+
const selectGetTabletsInfo = createSelector(
55+
(state: RootState) => state,
56+
(_state: RootState, path: string) => getTabletsInfoSelector(path),
57+
(state, selectTabletsInfo) => selectTabletsInfo(state).data,
58+
);
59+
60+
export const selectTabletsWithFqdn = createSelector(
61+
(state: RootState, path: string) => selectGetTabletsInfo(state, path),
62+
(state: RootState) => selectNodesMap(state),
63+
(data, nodesMap): (TTabletStateInfo & {fqdn?: string})[] => {
64+
if (!data?.TabletStateInfo) {
65+
return [];
66+
}
67+
if (!nodesMap) {
68+
return data.TabletStateInfo;
69+
}
70+
return data.TabletStateInfo.map((tablet) => {
71+
const fqdn = tablet.NodeId === undefined ? undefined : nodesMap.get(tablet.NodeId);
72+
return {...tablet, fqdn};
73+
});
74+
},
75+
);

src/utils/tablet.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type {LabelProps} from '@gravity-ui/uikit';
2+
13
import {EFlag} from '../types/api/enums';
24
import {ETabletState} from '../types/api/tablet';
35

@@ -52,3 +54,18 @@ export const mapTabletStateToColorState = (state?: ETabletState | EFlag): EFlag
5254

5355
return tabletStateToColorState[state];
5456
};
57+
58+
export function mapTabletStateToLabelTheme(state?: ETabletState): LabelProps['theme'] {
59+
if (!state) {
60+
return 'unknown';
61+
}
62+
switch (state) {
63+
case ETabletState.Dead:
64+
return 'danger';
65+
case ETabletState.Active:
66+
case ETabletState.Deleted:
67+
return 'success';
68+
default:
69+
return 'warning';
70+
}
71+
}

0 commit comments

Comments
 (0)