Skip to content

Commit a2b3555

Browse files
Copilotadameat
andcommitted
Implement VDisk tablets feature with tab navigation
Co-authored-by: adameat <[email protected]>
1 parent 491df59 commit a2b3555

File tree

11 files changed

+317
-22
lines changed

11 files changed

+317
-22
lines changed

src/containers/VDiskPage/VDiskPage.scss

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
&__title,
1313
&__controls,
1414
&__info,
15-
&__storage-title {
15+
&__tabs {
1616
position: sticky;
1717
left: 0;
1818

@@ -29,8 +29,7 @@
2929
gap: var(--g-spacing-2);
3030
}
3131

32-
&__storage-title {
33-
margin-bottom: 0;
34-
@include mixins.header-1-typography();
32+
&__tablets-content {
33+
margin-top: 16px;
3534
}
3635
}

src/containers/VDiskPage/VDiskPage.tsx

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

33
import {ArrowsOppositeToDots} from '@gravity-ui/icons';
4-
import {Icon} from '@gravity-ui/uikit';
4+
import {Icon, Tab, TabList, TabProvider} from '@gravity-ui/uikit';
55
import {skipToken} from '@reduxjs/toolkit/query';
66
import {Helmet} from 'react-helmet-async';
77
import {StringParam, useQueryParams} from 'use-query-params';
8+
import {z} from 'zod';
89

910
import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
1011
import {EntityPageTitle} from '../../components/EntityPageTitle/EntityPageTitle';
1112
import {ResponseError} from '../../components/Errors/ResponseError';
1213
import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewerSkeleton';
14+
import {InternalLink} from '../../components/InternalLink/InternalLink';
1315
import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta';
1416
import {VDiskInfo} from '../../components/VDiskInfo/VDiskInfo';
17+
import {getVDiskPagePath} from '../../routes';
1518
import {api} from '../../store/reducers/api';
1619
import {useDiskPagesAvailable} from '../../store/reducers/capabilities/hooks';
1720
import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
@@ -25,26 +28,52 @@ import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks';
2528
import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges';
2629
import {PaginatedStorage} from '../Storage/PaginatedStorage';
2730

31+
import {VDiskTablets} from './VDiskTablets';
2832
import {vDiskPageKeyset} from './i18n';
2933

3034
import './VDiskPage.scss';
3135

3236
const vDiskPageCn = cn('ydb-vdisk-page');
3337

38+
const VDISK_TABS_IDS = {
39+
storage: 'storage',
40+
tablets: 'tablets',
41+
} as const;
42+
43+
const VDISK_PAGE_TABS = [
44+
{
45+
id: VDISK_TABS_IDS.storage,
46+
get title() {
47+
return vDiskPageKeyset('storage');
48+
},
49+
},
50+
{
51+
id: VDISK_TABS_IDS.tablets,
52+
get title() {
53+
return vDiskPageKeyset('tablets');
54+
},
55+
},
56+
];
57+
58+
const vDiskTabSchema = z.nativeEnum(VDISK_TABS_IDS).catch(VDISK_TABS_IDS.storage);
59+
3460
export function VDiskPage() {
3561
const dispatch = useTypedDispatch();
3662

3763
const containerRef = React.useRef<HTMLDivElement>(null);
3864
const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges();
3965
const newDiskApiAvailable = useDiskPagesAvailable();
4066

41-
const [{nodeId, pDiskId, vDiskSlotId, vDiskId: vDiskIdParam}] = useQueryParams({
67+
const [{nodeId, pDiskId, vDiskSlotId, vDiskId: vDiskIdParam, activeTab}] = useQueryParams({
4268
nodeId: StringParam,
4369
pDiskId: StringParam,
4470
vDiskSlotId: StringParam,
4571
vDiskId: StringParam,
72+
activeTab: StringParam,
4673
});
4774

75+
const vDiskTab = vDiskTabSchema.parse(activeTab);
76+
4877
React.useEffect(() => {
4978
dispatch(setHeaderBreadcrumbs('vDisk', {nodeId, pDiskId, vDiskSlotId}));
5079
}, [dispatch, nodeId, pDiskId, vDiskSlotId]);
@@ -185,24 +214,67 @@ export function VDiskPage() {
185214
return <VDiskInfo data={vDiskData} className={vDiskPageCn('info')} wrap />;
186215
};
187216

217+
const renderTabs = () => {
218+
const vDiskParamsDefined =
219+
valueIsDefined(nodeId) && valueIsDefined(pDiskId) && valueIsDefined(vDiskSlotId);
220+
221+
return (
222+
<div className={vDiskPageCn('tabs')}>
223+
<TabProvider value={vDiskTab}>
224+
<TabList size="l">
225+
{VDISK_PAGE_TABS.map(({id, title}) => {
226+
const path = vDiskParamsDefined
227+
? getVDiskPagePath({nodeId, pDiskId, vDiskSlotId}, {activeTab: id})
228+
: undefined;
229+
return (
230+
<Tab key={id} value={id}>
231+
<InternalLink as="tab" to={path}>
232+
{title}
233+
</InternalLink>
234+
</Tab>
235+
);
236+
})}
237+
</TabList>
238+
</TabProvider>
239+
</div>
240+
);
241+
};
242+
243+
const renderTabsContent = () => {
244+
switch (vDiskTab) {
245+
case 'storage': {
246+
return renderStorageInfo();
247+
}
248+
case 'tablets': {
249+
return (
250+
<VDiskTablets
251+
nodeId={nodeId ?? undefined}
252+
pDiskId={pDiskId ?? undefined}
253+
vDiskSlotId={vDiskSlotId ?? undefined}
254+
className={vDiskPageCn('tablets-content')}
255+
/>
256+
);
257+
}
258+
default:
259+
return null;
260+
}
261+
};
262+
188263
const renderStorageInfo = () => {
189264
if (valueIsDefined(GroupID) && valueIsDefined(nodeId)) {
190265
return (
191-
<React.Fragment>
192-
<div className={vDiskPageCn('storage-title')}>{vDiskPageKeyset('storage')}</div>
193-
<PaginatedStorage
194-
groupId={GroupID}
195-
nodeId={nodeId}
196-
pDiskId={pDiskId ?? undefined}
197-
scrollContainerRef={containerRef}
198-
viewContext={{
199-
groupId: GroupID?.toString(),
200-
nodeId: nodeId?.toString(),
201-
pDiskId: pDiskId?.toString(),
202-
vDiskSlotId: vDiskSlotId?.toString(),
203-
}}
204-
/>
205-
</React.Fragment>
266+
<PaginatedStorage
267+
groupId={GroupID}
268+
nodeId={nodeId}
269+
pDiskId={pDiskId ?? undefined}
270+
scrollContainerRef={containerRef}
271+
viewContext={{
272+
groupId: GroupID?.toString(),
273+
nodeId: nodeId?.toString(),
274+
pDiskId: pDiskId?.toString(),
275+
vDiskSlotId: vDiskSlotId?.toString(),
276+
}}
277+
/>
206278
);
207279
}
208280

@@ -218,7 +290,8 @@ export function VDiskPage() {
218290
<React.Fragment>
219291
{error ? <ResponseError error={error} /> : null}
220292
{renderInfo()}
221-
{renderStorageInfo()}
293+
{renderTabs()}
294+
{renderTabsContent()}
222295
</React.Fragment>
223296
);
224297
};
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.ydb-vdisk-tablets {
2+
&__error {
3+
padding: 16px;
4+
5+
text-align: center;
6+
7+
color: var(--g-color-text-danger);
8+
}
9+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import React from 'react';
2+
3+
import {useTable} from '@gravity-ui/table';
4+
import {skipToken} from '@reduxjs/toolkit/query';
5+
6+
import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton';
7+
import {Table} from '../../../components/Table/Table';
8+
import {vDiskApi} from '../../../store/reducers/vdisk/vdisk';
9+
import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex';
10+
import {cn} from '../../../utils/cn';
11+
import {useAutoRefreshInterval} from '../../../utils/hooks';
12+
13+
import {getColumns} from './columns';
14+
15+
import './VDiskTablets.scss';
16+
17+
const vDiskTabletsCn = cn('ydb-vdisk-tablets');
18+
19+
interface VDiskTabletsProps {
20+
nodeId?: string | number;
21+
pDiskId?: string | number;
22+
vDiskSlotId?: string | number;
23+
className?: string;
24+
}
25+
26+
export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTabletsProps) {
27+
const [autoRefreshInterval] = useAutoRefreshInterval();
28+
29+
const params = nodeId && pDiskId && vDiskSlotId ? {nodeId, pDiskId, vDiskSlotId} : skipToken;
30+
31+
const {currentData, isFetching, error} = vDiskApi.useGetVDiskBlobIndexStatQuery(params, {
32+
pollingInterval: autoRefreshInterval,
33+
});
34+
35+
const loading = isFetching && currentData === undefined;
36+
const tableData: VDiskBlobIndexItem[] = currentData?.BlobIndexStat || [];
37+
38+
// Sort by size descending by default
39+
const sortedData = React.useMemo(() => {
40+
return [...tableData].sort((a, b) => {
41+
const sizeA = Number(a.Size) || 0;
42+
const sizeB = Number(b.Size) || 0;
43+
return sizeB - sizeA;
44+
});
45+
}, [tableData]);
46+
47+
const columns = React.useMemo(() => getColumns(), []);
48+
49+
const table = useTable({
50+
columns,
51+
data: sortedData,
52+
});
53+
54+
if (error) {
55+
return (
56+
<div className={vDiskTabletsCn('error', className)}>
57+
Error loading tablet statistics
58+
</div>
59+
);
60+
}
61+
62+
if (loading) {
63+
return <InfoViewerSkeleton rows={5} />;
64+
}
65+
66+
return (
67+
<div className={vDiskTabletsCn(null, className)}>
68+
<Table table={table} />
69+
</div>
70+
);
71+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type {CellContext, ColumnDef} from '@tanstack/react-table';
2+
3+
import {InternalLink} from '../../../components/InternalLink/InternalLink';
4+
import {ColumnHeader} from '../../../components/Table/Table';
5+
import {getTabletPagePath} from '../../../routes';
6+
import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex';
7+
import {cn} from '../../../utils/cn';
8+
import {formatBytes} from '../../../utils/dataFormatters/dataFormatters';
9+
import {vDiskPageKeyset} from '../i18n';
10+
11+
const b = cn('ydb-vdisk-tablets');
12+
13+
function TabletIdCell({getValue}: CellContext<VDiskBlobIndexItem, unknown>) {
14+
const tabletId = getValue<string | number>();
15+
16+
if (!tabletId) {
17+
return <span>-</span>;
18+
}
19+
20+
return <InternalLink to={getTabletPagePath(String(tabletId))}>{tabletId}</InternalLink>;
21+
}
22+
23+
function MetricsCell({getValue}: CellContext<VDiskBlobIndexItem, unknown>) {
24+
const value = getValue<string | number>();
25+
return <span className={b('metrics-cell')}>{value ?? '-'}</span>;
26+
}
27+
28+
function SizeCell({getValue}: CellContext<VDiskBlobIndexItem, unknown>) {
29+
const size = getValue<string | number>();
30+
const numericSize = Number(size) || 0;
31+
return <span className={b('size-cell')}>{formatBytes(numericSize)}</span>;
32+
}
33+
34+
export function getColumns() {
35+
const columns: ColumnDef<VDiskBlobIndexItem>[] = [
36+
{
37+
accessorKey: 'TabletId',
38+
header: () => <ColumnHeader>{vDiskPageKeyset('tablet-id')}</ColumnHeader>,
39+
size: 150,
40+
cell: TabletIdCell,
41+
},
42+
{
43+
accessorKey: 'ChannelId',
44+
header: () => <ColumnHeader>{vDiskPageKeyset('channel-id')}</ColumnHeader>,
45+
size: 100,
46+
cell: MetricsCell,
47+
meta: {align: 'right'},
48+
},
49+
{
50+
accessorKey: 'Count',
51+
header: () => <ColumnHeader>{vDiskPageKeyset('count')}</ColumnHeader>,
52+
size: 100,
53+
cell: MetricsCell,
54+
meta: {align: 'right'},
55+
},
56+
{
57+
accessorKey: 'Size',
58+
header: () => <ColumnHeader>{vDiskPageKeyset('size')}</ColumnHeader>,
59+
size: 120,
60+
cell: SizeCell,
61+
meta: {align: 'right'},
62+
},
63+
];
64+
65+
return columns;
66+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {VDiskTablets} from './VDiskTablets';

src/containers/VDiskPage/i18n/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
"pdisk": "PDisk",
55
"vdisk": "VDisk",
66
"storage": "Storage",
7+
"tablets": "Tablets",
8+
9+
"tablet-id": "Tablet ID",
10+
"channel-id": "Channel ID",
11+
"count": "Count",
12+
"size": "Size",
713

814
"evict-vdisk-button": "Evict VDisk",
915
"force-evict-vdisk-button": "Evict anyway",

src/services/api/viewer.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import type {
3434
import type {TTenantInfo, TTenants} from '../../types/api/tenant';
3535
import type {DescribeTopicResult, TopicDataRequest, TopicDataResponse} from '../../types/api/topic';
3636
import type {TEvVDiskStateResponse} from '../../types/api/vdisk';
37+
import type {VDiskBlobIndexResponse} from '../../types/api/vdiskBlobIndex';
3738
import type {TUserToken} from '../../types/api/whoami';
3839
import type {TabletsApiRequestParams} from '../../types/store/tablets';
3940
import {BINARY_DATA_IN_PLAIN_TEXT_DISPLAY} from '../../utils/constants';
@@ -536,6 +537,29 @@ export class ViewerAPI extends BaseYdbAPI {
536537
);
537538
}
538539

540+
getVDiskBlobIndexStat(
541+
{
542+
vDiskSlotId,
543+
pDiskId,
544+
nodeId,
545+
}: {
546+
vDiskSlotId: string | number;
547+
pDiskId: string | number;
548+
nodeId: string | number;
549+
},
550+
{concurrentId, signal}: AxiosOptions = {},
551+
) {
552+
return this.get<VDiskBlobIndexResponse>(
553+
this.getPath('/vdisk/blobindexstat'),
554+
{
555+
node_id: nodeId,
556+
pdisk_id: pDiskId,
557+
vslot_id: vDiskSlotId,
558+
},
559+
{concurrentId, requestConfig: {signal}},
560+
);
561+
}
562+
539563
getNodeWhiteboardPDiskInfo(
540564
{nodeId, pDiskId}: {nodeId: string | number; pDiskId: string | number},
541565
{concurrentId, signal}: AxiosOptions = {},

src/store/reducers/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const api = createApi({
1818
'Tablet',
1919
'UserData',
2020
'VDiskData',
21+
'VDiskBlobIndexStat',
2122
'AccessRights',
2223
'Backups',
2324
'BackupsSchedule',

0 commit comments

Comments
 (0)