From 491df5957f465265e0414c77d430a1a4446a8634 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:17:19 +0000 Subject: [PATCH 1/8] Initial plan From a2b355566fa8cbded5781725bf47c82d00fcfab0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 16 Jul 2025 11:37:59 +0000 Subject: [PATCH 2/8] Implement VDisk tablets feature with tab navigation Co-authored-by: adameat <34044711+adameat@users.noreply.github.com> --- src/containers/VDiskPage/VDiskPage.scss | 7 +- src/containers/VDiskPage/VDiskPage.tsx | 109 +++++++++++++++--- .../VDiskPage/VDiskTablets/VDiskTablets.scss | 9 ++ .../VDiskPage/VDiskTablets/VDiskTablets.tsx | 71 ++++++++++++ .../VDiskPage/VDiskTablets/columns.tsx | 66 +++++++++++ .../VDiskPage/VDiskTablets/index.ts | 1 + src/containers/VDiskPage/i18n/en.json | 6 + src/services/api/viewer.ts | 24 ++++ src/store/reducers/api.ts | 1 + src/store/reducers/vdisk/vdisk.ts | 20 ++++ src/types/api/vdiskBlobIndex.ts | 25 ++++ 11 files changed, 317 insertions(+), 22 deletions(-) create mode 100644 src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss create mode 100644 src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx create mode 100644 src/containers/VDiskPage/VDiskTablets/columns.tsx create mode 100644 src/containers/VDiskPage/VDiskTablets/index.ts create mode 100644 src/types/api/vdiskBlobIndex.ts diff --git a/src/containers/VDiskPage/VDiskPage.scss b/src/containers/VDiskPage/VDiskPage.scss index 5af1027dc3..eb752d819f 100644 --- a/src/containers/VDiskPage/VDiskPage.scss +++ b/src/containers/VDiskPage/VDiskPage.scss @@ -12,7 +12,7 @@ &__title, &__controls, &__info, - &__storage-title { + &__tabs { position: sticky; left: 0; @@ -29,8 +29,7 @@ gap: var(--g-spacing-2); } - &__storage-title { - margin-bottom: 0; - @include mixins.header-1-typography(); + &__tablets-content { + margin-top: 16px; } } diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index c9371c001a..35155c5fa2 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -1,17 +1,20 @@ import React from 'react'; import {ArrowsOppositeToDots} from '@gravity-ui/icons'; -import {Icon} from '@gravity-ui/uikit'; +import {Icon, Tab, TabList, TabProvider} from '@gravity-ui/uikit'; import {skipToken} from '@reduxjs/toolkit/query'; import {Helmet} from 'react-helmet-async'; import {StringParam, useQueryParams} from 'use-query-params'; +import {z} from 'zod'; import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; import {EntityPageTitle} from '../../components/EntityPageTitle/EntityPageTitle'; import {ResponseError} from '../../components/Errors/ResponseError'; import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewerSkeleton'; +import {InternalLink} from '../../components/InternalLink/InternalLink'; import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta'; import {VDiskInfo} from '../../components/VDiskInfo/VDiskInfo'; +import {getVDiskPagePath} from '../../routes'; import {api} from '../../store/reducers/api'; import {useDiskPagesAvailable} from '../../store/reducers/capabilities/hooks'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; @@ -25,12 +28,35 @@ import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks'; import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {PaginatedStorage} from '../Storage/PaginatedStorage'; +import {VDiskTablets} from './VDiskTablets'; import {vDiskPageKeyset} from './i18n'; import './VDiskPage.scss'; const vDiskPageCn = cn('ydb-vdisk-page'); +const VDISK_TABS_IDS = { + storage: 'storage', + tablets: 'tablets', +} as const; + +const VDISK_PAGE_TABS = [ + { + id: VDISK_TABS_IDS.storage, + get title() { + return vDiskPageKeyset('storage'); + }, + }, + { + id: VDISK_TABS_IDS.tablets, + get title() { + return vDiskPageKeyset('tablets'); + }, + }, +]; + +const vDiskTabSchema = z.nativeEnum(VDISK_TABS_IDS).catch(VDISK_TABS_IDS.storage); + export function VDiskPage() { const dispatch = useTypedDispatch(); @@ -38,13 +64,16 @@ export function VDiskPage() { const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); const newDiskApiAvailable = useDiskPagesAvailable(); - const [{nodeId, pDiskId, vDiskSlotId, vDiskId: vDiskIdParam}] = useQueryParams({ + const [{nodeId, pDiskId, vDiskSlotId, vDiskId: vDiskIdParam, activeTab}] = useQueryParams({ nodeId: StringParam, pDiskId: StringParam, vDiskSlotId: StringParam, vDiskId: StringParam, + activeTab: StringParam, }); + const vDiskTab = vDiskTabSchema.parse(activeTab); + React.useEffect(() => { dispatch(setHeaderBreadcrumbs('vDisk', {nodeId, pDiskId, vDiskSlotId})); }, [dispatch, nodeId, pDiskId, vDiskSlotId]); @@ -185,24 +214,67 @@ export function VDiskPage() { return ; }; + const renderTabs = () => { + const vDiskParamsDefined = + valueIsDefined(nodeId) && valueIsDefined(pDiskId) && valueIsDefined(vDiskSlotId); + + return ( +
+ + + {VDISK_PAGE_TABS.map(({id, title}) => { + const path = vDiskParamsDefined + ? getVDiskPagePath({nodeId, pDiskId, vDiskSlotId}, {activeTab: id}) + : undefined; + return ( + + + {title} + + + ); + })} + + +
+ ); + }; + + const renderTabsContent = () => { + switch (vDiskTab) { + case 'storage': { + return renderStorageInfo(); + } + case 'tablets': { + return ( + + ); + } + default: + return null; + } + }; + const renderStorageInfo = () => { if (valueIsDefined(GroupID) && valueIsDefined(nodeId)) { return ( - -
{vDiskPageKeyset('storage')}
- -
+ ); } @@ -218,7 +290,8 @@ export function VDiskPage() { {error ? : null} {renderInfo()} - {renderStorageInfo()} + {renderTabs()} + {renderTabsContent()} ); }; diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss new file mode 100644 index 0000000000..6776954888 --- /dev/null +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss @@ -0,0 +1,9 @@ +.ydb-vdisk-tablets { + &__error { + padding: 16px; + + text-align: center; + + color: var(--g-color-text-danger); + } +} diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx new file mode 100644 index 0000000000..91da27cf92 --- /dev/null +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +import {useTable} from '@gravity-ui/table'; +import {skipToken} from '@reduxjs/toolkit/query'; + +import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton'; +import {Table} from '../../../components/Table/Table'; +import {vDiskApi} from '../../../store/reducers/vdisk/vdisk'; +import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex'; +import {cn} from '../../../utils/cn'; +import {useAutoRefreshInterval} from '../../../utils/hooks'; + +import {getColumns} from './columns'; + +import './VDiskTablets.scss'; + +const vDiskTabletsCn = cn('ydb-vdisk-tablets'); + +interface VDiskTabletsProps { + nodeId?: string | number; + pDiskId?: string | number; + vDiskSlotId?: string | number; + className?: string; +} + +export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTabletsProps) { + const [autoRefreshInterval] = useAutoRefreshInterval(); + + const params = nodeId && pDiskId && vDiskSlotId ? {nodeId, pDiskId, vDiskSlotId} : skipToken; + + const {currentData, isFetching, error} = vDiskApi.useGetVDiskBlobIndexStatQuery(params, { + pollingInterval: autoRefreshInterval, + }); + + const loading = isFetching && currentData === undefined; + const tableData: VDiskBlobIndexItem[] = currentData?.BlobIndexStat || []; + + // Sort by size descending by default + const sortedData = React.useMemo(() => { + return [...tableData].sort((a, b) => { + const sizeA = Number(a.Size) || 0; + const sizeB = Number(b.Size) || 0; + return sizeB - sizeA; + }); + }, [tableData]); + + const columns = React.useMemo(() => getColumns(), []); + + const table = useTable({ + columns, + data: sortedData, + }); + + if (error) { + return ( +
+ Error loading tablet statistics +
+ ); + } + + if (loading) { + return ; + } + + return ( +
+ + + ); +} diff --git a/src/containers/VDiskPage/VDiskTablets/columns.tsx b/src/containers/VDiskPage/VDiskTablets/columns.tsx new file mode 100644 index 0000000000..653231292b --- /dev/null +++ b/src/containers/VDiskPage/VDiskTablets/columns.tsx @@ -0,0 +1,66 @@ +import type {CellContext, ColumnDef} from '@tanstack/react-table'; + +import {InternalLink} from '../../../components/InternalLink/InternalLink'; +import {ColumnHeader} from '../../../components/Table/Table'; +import {getTabletPagePath} from '../../../routes'; +import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex'; +import {cn} from '../../../utils/cn'; +import {formatBytes} from '../../../utils/dataFormatters/dataFormatters'; +import {vDiskPageKeyset} from '../i18n'; + +const b = cn('ydb-vdisk-tablets'); + +function TabletIdCell({getValue}: CellContext) { + const tabletId = getValue(); + + if (!tabletId) { + return -; + } + + return {tabletId}; +} + +function MetricsCell({getValue}: CellContext) { + const value = getValue(); + return {value ?? '-'}; +} + +function SizeCell({getValue}: CellContext) { + const size = getValue(); + const numericSize = Number(size) || 0; + return {formatBytes(numericSize)}; +} + +export function getColumns() { + const columns: ColumnDef[] = [ + { + accessorKey: 'TabletId', + header: () => {vDiskPageKeyset('tablet-id')}, + size: 150, + cell: TabletIdCell, + }, + { + accessorKey: 'ChannelId', + header: () => {vDiskPageKeyset('channel-id')}, + size: 100, + cell: MetricsCell, + meta: {align: 'right'}, + }, + { + accessorKey: 'Count', + header: () => {vDiskPageKeyset('count')}, + size: 100, + cell: MetricsCell, + meta: {align: 'right'}, + }, + { + accessorKey: 'Size', + header: () => {vDiskPageKeyset('size')}, + size: 120, + cell: SizeCell, + meta: {align: 'right'}, + }, + ]; + + return columns; +} diff --git a/src/containers/VDiskPage/VDiskTablets/index.ts b/src/containers/VDiskPage/VDiskTablets/index.ts new file mode 100644 index 0000000000..9e3955d879 --- /dev/null +++ b/src/containers/VDiskPage/VDiskTablets/index.ts @@ -0,0 +1 @@ +export {VDiskTablets} from './VDiskTablets'; diff --git a/src/containers/VDiskPage/i18n/en.json b/src/containers/VDiskPage/i18n/en.json index fe10fe8f47..1348157388 100644 --- a/src/containers/VDiskPage/i18n/en.json +++ b/src/containers/VDiskPage/i18n/en.json @@ -4,6 +4,12 @@ "pdisk": "PDisk", "vdisk": "VDisk", "storage": "Storage", + "tablets": "Tablets", + + "tablet-id": "Tablet ID", + "channel-id": "Channel ID", + "count": "Count", + "size": "Size", "evict-vdisk-button": "Evict VDisk", "force-evict-vdisk-button": "Evict anyway", diff --git a/src/services/api/viewer.ts b/src/services/api/viewer.ts index 3ff38a1665..45c2c0745a 100644 --- a/src/services/api/viewer.ts +++ b/src/services/api/viewer.ts @@ -34,6 +34,7 @@ import type { import type {TTenantInfo, TTenants} from '../../types/api/tenant'; import type {DescribeTopicResult, TopicDataRequest, TopicDataResponse} from '../../types/api/topic'; import type {TEvVDiskStateResponse} from '../../types/api/vdisk'; +import type {VDiskBlobIndexResponse} from '../../types/api/vdiskBlobIndex'; import type {TUserToken} from '../../types/api/whoami'; import type {TabletsApiRequestParams} from '../../types/store/tablets'; import {BINARY_DATA_IN_PLAIN_TEXT_DISPLAY} from '../../utils/constants'; @@ -536,6 +537,29 @@ export class ViewerAPI extends BaseYdbAPI { ); } + getVDiskBlobIndexStat( + { + vDiskSlotId, + pDiskId, + nodeId, + }: { + vDiskSlotId: string | number; + pDiskId: string | number; + nodeId: string | number; + }, + {concurrentId, signal}: AxiosOptions = {}, + ) { + return this.get( + this.getPath('/vdisk/blobindexstat'), + { + node_id: nodeId, + pdisk_id: pDiskId, + vslot_id: vDiskSlotId, + }, + {concurrentId, requestConfig: {signal}}, + ); + } + getNodeWhiteboardPDiskInfo( {nodeId, pDiskId}: {nodeId: string | number; pDiskId: string | number}, {concurrentId, signal}: AxiosOptions = {}, diff --git a/src/store/reducers/api.ts b/src/store/reducers/api.ts index 441d28f22f..627c4d3e39 100644 --- a/src/store/reducers/api.ts +++ b/src/store/reducers/api.ts @@ -18,6 +18,7 @@ export const api = createApi({ 'Tablet', 'UserData', 'VDiskData', + 'VDiskBlobIndexStat', 'AccessRights', 'Backups', 'BackupsSchedule', diff --git a/src/store/reducers/vdisk/vdisk.ts b/src/store/reducers/vdisk/vdisk.ts index 4f5964a8c0..10e65703d2 100644 --- a/src/store/reducers/vdisk/vdisk.ts +++ b/src/store/reducers/vdisk/vdisk.ts @@ -33,6 +33,26 @@ export const vDiskApi = api.injectEndpoints({ }, ], }), + getVDiskBlobIndexStat: build.query({ + queryFn: async ({nodeId, pDiskId, vDiskSlotId}: VDiskDataRequestParams, {signal}) => { + try { + const response = await window.api.viewer.getVDiskBlobIndexStat( + {nodeId, pDiskId, vDiskSlotId}, + {signal}, + ); + return {data: response}; + } catch (error) { + return {error}; + } + }, + providesTags: (_result, _error, arg) => [ + 'All', + { + type: 'VDiskBlobIndexStat', + id: getVDiskSlotBasedId(arg.nodeId, arg.pDiskId, arg.vDiskSlotId), + }, + ], + }), }), overrideExisting: 'throw', }); diff --git a/src/types/api/vdiskBlobIndex.ts b/src/types/api/vdiskBlobIndex.ts new file mode 100644 index 0000000000..4f9580bd01 --- /dev/null +++ b/src/types/api/vdiskBlobIndex.ts @@ -0,0 +1,25 @@ +/** + * VDisk Blob Index Statistics API types + * + * endpoint: /vdisk/blobindexstat + */ + +export interface VDiskBlobIndexItem { + /** Tablet ID */ + TabletId?: string | number; + /** Channel ID */ + ChannelId?: number; + /** Count */ + Count?: number; + /** Size in bytes */ + Size?: number | string; +} + +export interface VDiskBlobIndexResponse { + /** Array of blob index statistics */ + BlobIndexStat?: VDiskBlobIndexItem[]; + /** Response time */ + ResponseTime?: string; + /** Response duration */ + ResponseDuration?: number; +} From e3971751a720f62725c99bd87ddc17b461ea6e9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:18:55 +0000 Subject: [PATCH 3/8] Fix VDisk tablets table display issue by handling multiple API response structures Co-authored-by: adameat <34044711+adameat@users.noreply.github.com> --- .../VDiskPage/VDiskTablets/VDiskTablets.tsx | 50 +++++++++++++++++-- .../VDiskPage/VDiskTablets/columns.tsx | 26 +++++++--- src/types/api/vdiskBlobIndex.ts | 18 +++++++ 3 files changed, 85 insertions(+), 9 deletions(-) diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx index 91da27cf92..426f168c5e 100644 --- a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx @@ -33,13 +33,57 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab }); const loading = isFetching && currentData === undefined; - const tableData: VDiskBlobIndexItem[] = currentData?.BlobIndexStat || []; + + // Debug: Log the actual response to understand the structure + React.useEffect(() => { + if (currentData) { + console.log('VDisk BlobIndexStat Response:', currentData); + console.log('Response keys:', Object.keys(currentData)); + console.log('BlobIndexStat field:', currentData.BlobIndexStat); + console.log('blobIndexStat field:', currentData.blobIndexStat); + console.log('blobindexstat field:', currentData.blobindexstat); + console.log('result field:', currentData.result); + console.log('data field:', currentData.data); + } + }, [currentData]); + + // Try multiple possible field names for the data array + const tableData: VDiskBlobIndexItem[] = React.useMemo(() => { + if (!currentData) return []; + + // Try different possible field names + const possibleFields = [ + currentData.BlobIndexStat, + currentData.blobIndexStat, + currentData.blobindexstat, + currentData.result, + currentData.data, + ]; + + for (const field of possibleFields) { + if (Array.isArray(field)) { + console.log('Using field:', field); + return field; + } + } + + // If none of the expected fields work, try to find any array in the response + for (const [key, value] of Object.entries(currentData)) { + if (Array.isArray(value)) { + console.log('Found array field:', key, value); + return value; + } + } + + console.log('No array found in response, returning empty array'); + return []; + }, [currentData]); // Sort by size descending by default const sortedData = React.useMemo(() => { return [...tableData].sort((a, b) => { - const sizeA = Number(a.Size) || 0; - const sizeB = Number(b.Size) || 0; + const sizeA = Number(a.Size ?? a.size) || 0; + const sizeB = Number(b.Size ?? b.size) || 0; return sizeB - sizeA; }); }, [tableData]); diff --git a/src/containers/VDiskPage/VDiskTablets/columns.tsx b/src/containers/VDiskPage/VDiskTablets/columns.tsx index 653231292b..f1eeff5649 100644 --- a/src/containers/VDiskPage/VDiskTablets/columns.tsx +++ b/src/containers/VDiskPage/VDiskTablets/columns.tsx @@ -10,8 +10,9 @@ import {vDiskPageKeyset} from '../i18n'; const b = cn('ydb-vdisk-tablets'); -function TabletIdCell({getValue}: CellContext) { - const tabletId = getValue(); +function TabletIdCell({row}: CellContext) { + const item = row.original; + const tabletId = item.TabletId || item.tabletId; if (!tabletId) { return -; @@ -20,13 +21,26 @@ function TabletIdCell({getValue}: CellContext) { return {tabletId}; } -function MetricsCell({getValue}: CellContext) { - const value = getValue(); +function MetricsCell({row, column}: CellContext) { + const item = row.original; + const fieldName = column.id; + + // Handle both PascalCase and camelCase field names + let value; + if (fieldName === 'ChannelId') { + value = item.ChannelId ?? item.channelId; + } else if (fieldName === 'Count') { + value = item.Count ?? item.count; + } else { + value = item[fieldName]; + } + return {value ?? '-'}; } -function SizeCell({getValue}: CellContext) { - const size = getValue(); +function SizeCell({row}: CellContext) { + const item = row.original; + const size = item.Size ?? item.size; const numericSize = Number(size) || 0; return {formatBytes(numericSize)}; } diff --git a/src/types/api/vdiskBlobIndex.ts b/src/types/api/vdiskBlobIndex.ts index 4f9580bd01..9ee36c5770 100644 --- a/src/types/api/vdiskBlobIndex.ts +++ b/src/types/api/vdiskBlobIndex.ts @@ -7,19 +7,37 @@ export interface VDiskBlobIndexItem { /** Tablet ID */ TabletId?: string | number; + /** Alternative field name for Tablet ID */ + tabletId?: string | number; /** Channel ID */ ChannelId?: number; + /** Alternative field name for Channel ID */ + channelId?: number; /** Count */ Count?: number; + /** Alternative field name for Count */ + count?: number; /** Size in bytes */ Size?: number | string; + /** Alternative field name for Size */ + size?: number | string; + /** Allow for other possible field names */ + [key: string]: any; } export interface VDiskBlobIndexResponse { /** Array of blob index statistics */ BlobIndexStat?: VDiskBlobIndexItem[]; + /** Alternative possible field name (camelCase) */ + blobIndexStat?: VDiskBlobIndexItem[]; + /** Alternative possible field name (lowercase) */ + blobindexstat?: VDiskBlobIndexItem[]; /** Response time */ ResponseTime?: string; /** Response duration */ ResponseDuration?: number; + /** Alternative response structures */ + result?: VDiskBlobIndexItem[]; + data?: VDiskBlobIndexItem[]; + [key: string]: any; // Allow for other possible field names } From 2626edefb888f9ef7f3b5bca22dd1620fe5302ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 11:44:33 +0000 Subject: [PATCH 4/8] Fix VDisk tablets table data parsing to handle actual API response structure Co-authored-by: adameat <34044711+adameat@users.noreply.github.com> --- .../VDiskPage/VDiskTablets/VDiskTablets.tsx | 83 +++++++++---------- src/types/api/vdiskBlobIndex.ts | 40 +++++++-- 2 files changed, 73 insertions(+), 50 deletions(-) diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx index 426f168c5e..cc33540a35 100644 --- a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx @@ -33,57 +33,54 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab }); const loading = isFetching && currentData === undefined; - - // Debug: Log the actual response to understand the structure - React.useEffect(() => { - if (currentData) { - console.log('VDisk BlobIndexStat Response:', currentData); - console.log('Response keys:', Object.keys(currentData)); - console.log('BlobIndexStat field:', currentData.BlobIndexStat); - console.log('blobIndexStat field:', currentData.blobIndexStat); - console.log('blobindexstat field:', currentData.blobindexstat); - console.log('result field:', currentData.result); - console.log('data field:', currentData.data); - } - }, [currentData]); - - // Try multiple possible field names for the data array + + // Transform the actual API response structure into the expected flat table format const tableData: VDiskBlobIndexItem[] = React.useMemo(() => { - if (!currentData) return []; - - // Try different possible field names - const possibleFields = [ - currentData.BlobIndexStat, - currentData.blobIndexStat, - currentData.blobindexstat, - currentData.result, - currentData.data, - ]; - - for (const field of possibleFields) { - if (Array.isArray(field)) { - console.log('Using field:', field); - return field; - } + if (!currentData) { + return []; } - - // If none of the expected fields work, try to find any array in the response - for (const [key, value] of Object.entries(currentData)) { - if (Array.isArray(value)) { - console.log('Found array field:', key, value); - return value; - } + + // Debug: Log the actual response structure + console.info('VDisk BlobIndexStat Response:', currentData); + + // Check if we have the expected structure: {stat: {tablets: [...]}} + const stat = currentData.stat; + if (!stat || !Array.isArray(stat.tablets)) { + console.info('No stat.tablets array found in response'); + return []; } - - console.log('No array found in response, returning empty array'); - return []; + + // Transform the nested structure into flat table rows + const flatData: VDiskBlobIndexItem[] = []; + + stat.tablets.forEach((tablet: any) => { + const tabletId = tablet.tablet_id; + if (!tabletId || !Array.isArray(tablet.channels)) { + return; // Skip tablets without ID or channels + } + + tablet.channels.forEach((channel: any, channelIndex: number) => { + // Only include channels that have count and data_size + if (channel.count && channel.data_size) { + flatData.push({ + TabletId: tabletId, + ChannelId: channelIndex, + Count: parseInt(channel.count, 10) || 0, + Size: parseInt(channel.data_size, 10) || 0, + }); + } + }); + }); + + console.info('Transformed data:', flatData); + return flatData; }, [currentData]); // Sort by size descending by default const sortedData = React.useMemo(() => { return [...tableData].sort((a, b) => { - const sizeA = Number(a.Size ?? a.size) || 0; - const sizeB = Number(b.Size ?? b.size) || 0; + const sizeA = Number(a.Size) || 0; + const sizeB = Number(b.Size) || 0; return sizeB - sizeA; }); }, [tableData]); diff --git a/src/types/api/vdiskBlobIndex.ts b/src/types/api/vdiskBlobIndex.ts index 9ee36c5770..97941c18d1 100644 --- a/src/types/api/vdiskBlobIndex.ts +++ b/src/types/api/vdiskBlobIndex.ts @@ -25,18 +25,44 @@ export interface VDiskBlobIndexItem { [key: string]: any; } +export interface VDiskBlobIndexChannel { + /** Channel count */ + count?: string; + /** Channel data size */ + data_size?: string; + /** Channel minimum ID */ + min_id?: string; + /** Channel maximum ID */ + max_id?: string; +} + +export interface VDiskBlobIndexTablet { + /** Tablet identifier */ + tablet_id?: string; + /** Array of tablet channels */ + channels?: VDiskBlobIndexChannel[]; +} + +export interface VDiskBlobIndexStat { + /** Array of tablets */ + tablets?: VDiskBlobIndexTablet[]; + /** Array of channels */ + channels?: VDiskBlobIndexChannel[]; +} + export interface VDiskBlobIndexResponse { - /** Array of blob index statistics */ - BlobIndexStat?: VDiskBlobIndexItem[]; - /** Alternative possible field name (camelCase) */ - blobIndexStat?: VDiskBlobIndexItem[]; - /** Alternative possible field name (lowercase) */ - blobindexstat?: VDiskBlobIndexItem[]; + /** Response status */ + status?: string; + /** Statistics data */ + stat?: VDiskBlobIndexStat; /** Response time */ ResponseTime?: string; /** Response duration */ ResponseDuration?: number; - /** Alternative response structures */ + /** Alternative response structures for backward compatibility */ + BlobIndexStat?: VDiskBlobIndexItem[]; + blobIndexStat?: VDiskBlobIndexItem[]; + blobindexstat?: VDiskBlobIndexItem[]; result?: VDiskBlobIndexItem[]; data?: VDiskBlobIndexItem[]; [key: string]: any; // Allow for other possible field names From 246d95c4c60991442b09bbbecae980653e5a1ad4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 12:33:57 +0000 Subject: [PATCH 5/8] Implement sticky header, resizable and sortable columns for VDisk tablets table Co-authored-by: Raubzeug <42094364+Raubzeug@users.noreply.github.com> --- .../VDiskPage/VDiskTablets/VDiskTablets.tsx | 26 ++++++++++--------- .../VDiskPage/VDiskTablets/columns.tsx | 13 ++++++++-- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx index cc33540a35..82127ccbd6 100644 --- a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {useTable} from '@gravity-ui/table'; import {skipToken} from '@reduxjs/toolkit/query'; +import type {SortingState} from '@tanstack/react-table'; import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton'; import {Table} from '../../../components/Table/Table'; @@ -25,6 +26,9 @@ interface VDiskTabletsProps { export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTabletsProps) { const [autoRefreshInterval] = useAutoRefreshInterval(); + const [sorting, setSorting] = React.useState([ + {id: 'Size', desc: true}, // Default sort by size descending + ]); const params = nodeId && pDiskId && vDiskSlotId ? {nodeId, pDiskId, vDiskSlotId} : skipToken; @@ -34,7 +38,7 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab const loading = isFetching && currentData === undefined; - // Transform the actual API response structure into the expected flat table format + // Remove the manual sorting since we'll let the table handle it with enableSorting const tableData: VDiskBlobIndexItem[] = React.useMemo(() => { if (!currentData) { return []; @@ -76,20 +80,18 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab return flatData; }, [currentData]); - // Sort by size descending by default - const sortedData = React.useMemo(() => { - return [...tableData].sort((a, b) => { - const sizeA = Number(a.Size) || 0; - const sizeB = Number(b.Size) || 0; - return sizeB - sizeA; - }); - }, [tableData]); - const columns = React.useMemo(() => getColumns(), []); const table = useTable({ columns, - data: sortedData, + data: tableData, + enableSorting: true, + enableColumnResizing: true, + columnResizeMode: 'onChange', + onSortingChange: setSorting, + state: { + sorting, + }, }); if (error) { @@ -106,7 +108,7 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab return (
-
+
); } diff --git a/src/containers/VDiskPage/VDiskTablets/columns.tsx b/src/containers/VDiskPage/VDiskTablets/columns.tsx index f1eeff5649..1ff75c3dec 100644 --- a/src/containers/VDiskPage/VDiskTablets/columns.tsx +++ b/src/containers/VDiskPage/VDiskTablets/columns.tsx @@ -24,7 +24,7 @@ function TabletIdCell({row}: CellContext) { function MetricsCell({row, column}: CellContext) { const item = row.original; const fieldName = column.id; - + // Handle both PascalCase and camelCase field names let value; if (fieldName === 'ChannelId') { @@ -34,7 +34,7 @@ function MetricsCell({row, column}: CellContext) { } else { value = item[fieldName]; } - + return {value ?? '-'}; } @@ -51,28 +51,37 @@ export function getColumns() { accessorKey: 'TabletId', header: () => {vDiskPageKeyset('tablet-id')}, size: 150, + minSize: 100, cell: TabletIdCell, + enableSorting: true, }, { accessorKey: 'ChannelId', header: () => {vDiskPageKeyset('channel-id')}, size: 100, + minSize: 80, cell: MetricsCell, meta: {align: 'right'}, + enableSorting: true, }, { accessorKey: 'Count', header: () => {vDiskPageKeyset('count')}, size: 100, + minSize: 80, cell: MetricsCell, meta: {align: 'right'}, + enableSorting: true, }, { accessorKey: 'Size', header: () => {vDiskPageKeyset('size')}, size: 120, + minSize: 100, cell: SizeCell, meta: {align: 'right'}, + enableSorting: true, + sortingFn: 'basic', // Use basic sorting for numeric values }, ]; From d5905f0b68780223fb858a5d950a4a50660ec9e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 17 Jul 2025 13:01:48 +0000 Subject: [PATCH 6/8] Refactor VDisk tablets table to use project patterns with ResizeableDataTable Co-authored-by: Raubzeug <42094364+Raubzeug@users.noreply.github.com> --- .../VDiskPage/VDiskTablets/VDiskTablets.tsx | 47 ++++---- .../VDiskPage/VDiskTablets/columns.tsx | 102 ++++++------------ 2 files changed, 52 insertions(+), 97 deletions(-) diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx index 82127ccbd6..4ef0573675 100644 --- a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx @@ -1,21 +1,29 @@ import React from 'react'; -import {useTable} from '@gravity-ui/table'; +import type {Column} from '@gravity-ui/react-data-table'; +import DataTable from '@gravity-ui/react-data-table'; import {skipToken} from '@reduxjs/toolkit/query'; -import type {SortingState} from '@tanstack/react-table'; import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton'; -import {Table} from '../../../components/Table/Table'; +import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable'; import {vDiskApi} from '../../../store/reducers/vdisk/vdisk'; import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex'; import {cn} from '../../../utils/cn'; import {useAutoRefreshInterval} from '../../../utils/hooks'; import {getColumns} from './columns'; +import {vDiskPageKeyset} from '../i18n'; import './VDiskTablets.scss'; const vDiskTabletsCn = cn('ydb-vdisk-tablets'); +const VDISK_TABLETS_COLUMNS_WIDTH_LS_KEY = 'vdiskTabletsColumnsWidth'; + +const TABLE_SETTINGS = { + displayIndices: false, + highlightRows: true, + stickyHead: DataTable.MOVING, +}; interface VDiskTabletsProps { nodeId?: string | number; @@ -26,9 +34,6 @@ interface VDiskTabletsProps { export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTabletsProps) { const [autoRefreshInterval] = useAutoRefreshInterval(); - const [sorting, setSorting] = React.useState([ - {id: 'Size', desc: true}, // Default sort by size descending - ]); const params = nodeId && pDiskId && vDiskSlotId ? {nodeId, pDiskId, vDiskSlotId} : skipToken; @@ -38,19 +43,14 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab const loading = isFetching && currentData === undefined; - // Remove the manual sorting since we'll let the table handle it with enableSorting const tableData: VDiskBlobIndexItem[] = React.useMemo(() => { if (!currentData) { return []; } - // Debug: Log the actual response structure - console.info('VDisk BlobIndexStat Response:', currentData); - // Check if we have the expected structure: {stat: {tablets: [...]}} const stat = currentData.stat; if (!stat || !Array.isArray(stat.tablets)) { - console.info('No stat.tablets array found in response'); return []; } @@ -76,24 +76,11 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab }); }); - console.info('Transformed data:', flatData); return flatData; }, [currentData]); const columns = React.useMemo(() => getColumns(), []); - const table = useTable({ - columns, - data: tableData, - enableSorting: true, - enableColumnResizing: true, - columnResizeMode: 'onChange', - onSortingChange: setSorting, - state: { - sorting, - }, - }); - if (error) { return (
@@ -108,7 +95,17 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab return (
-
+ ); } diff --git a/src/containers/VDiskPage/VDiskTablets/columns.tsx b/src/containers/VDiskPage/VDiskTablets/columns.tsx index 1ff75c3dec..222498d457 100644 --- a/src/containers/VDiskPage/VDiskTablets/columns.tsx +++ b/src/containers/VDiskPage/VDiskTablets/columns.tsx @@ -1,89 +1,47 @@ -import type {CellContext, ColumnDef} from '@tanstack/react-table'; +import type {Column} from '@gravity-ui/react-data-table'; +import DataTable from '@gravity-ui/react-data-table'; import {InternalLink} from '../../../components/InternalLink/InternalLink'; -import {ColumnHeader} from '../../../components/Table/Table'; import {getTabletPagePath} from '../../../routes'; import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex'; -import {cn} from '../../../utils/cn'; import {formatBytes} from '../../../utils/dataFormatters/dataFormatters'; import {vDiskPageKeyset} from '../i18n'; -const b = cn('ydb-vdisk-tablets'); - -function TabletIdCell({row}: CellContext) { - const item = row.original; - const tabletId = item.TabletId || item.tabletId; - - if (!tabletId) { - return -; - } - - return {tabletId}; -} - -function MetricsCell({row, column}: CellContext) { - const item = row.original; - const fieldName = column.id; - - // Handle both PascalCase and camelCase field names - let value; - if (fieldName === 'ChannelId') { - value = item.ChannelId ?? item.channelId; - } else if (fieldName === 'Count') { - value = item.Count ?? item.count; - } else { - value = item[fieldName]; - } - - return {value ?? '-'}; -} - -function SizeCell({row}: CellContext) { - const item = row.original; - const size = item.Size ?? item.size; - const numericSize = Number(size) || 0; - return {formatBytes(numericSize)}; -} - -export function getColumns() { - const columns: ColumnDef[] = [ +export function getColumns(): Column[] { + return [ { - accessorKey: 'TabletId', - header: () => {vDiskPageKeyset('tablet-id')}, - size: 150, - minSize: 100, - cell: TabletIdCell, - enableSorting: true, + name: vDiskPageKeyset('tablet-id'), + render: ({row}) => { + const tabletId = row.TabletId; + if (!tabletId) { + return -; + } + return {tabletId}; + }, + width: 150, }, { - accessorKey: 'ChannelId', - header: () => {vDiskPageKeyset('channel-id')}, - size: 100, - minSize: 80, - cell: MetricsCell, - meta: {align: 'right'}, - enableSorting: true, + name: vDiskPageKeyset('channel-id'), + align: DataTable.RIGHT, + render: ({row}) => row.ChannelId ?? '-', + width: 100, }, { - accessorKey: 'Count', - header: () => {vDiskPageKeyset('count')}, - size: 100, - minSize: 80, - cell: MetricsCell, - meta: {align: 'right'}, - enableSorting: true, + name: vDiskPageKeyset('count'), + align: DataTable.RIGHT, + render: ({row}) => row.Count ?? '-', + width: 100, }, { - accessorKey: 'Size', - header: () => {vDiskPageKeyset('size')}, - size: 120, - minSize: 100, - cell: SizeCell, - meta: {align: 'right'}, - enableSorting: true, - sortingFn: 'basic', // Use basic sorting for numeric values + name: vDiskPageKeyset('size'), + align: DataTable.RIGHT, + render: ({row}) => { + const size = row.Size; + const numericSize = Number(size) || 0; + return formatBytes(numericSize); + }, + width: 120, + sortAccessor: (row) => row.Size || 0, }, ]; - - return columns; } From 82bf170f58b808fa38cba53135becc3a91bc4d84 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Thu, 17 Jul 2025 20:11:07 +0300 Subject: [PATCH 7/8] fix: small sixes --- src/containers/VDiskPage/VDiskPage.scss | 2 +- .../VDiskPage/VDiskTablets/VDiskTablets.scss | 9 ----- .../VDiskPage/VDiskTablets/VDiskTablets.tsx | 36 +++++++------------ .../VDiskPage/VDiskTablets/columns.tsx | 29 ++++++++++----- 4 files changed, 34 insertions(+), 42 deletions(-) diff --git a/src/containers/VDiskPage/VDiskPage.scss b/src/containers/VDiskPage/VDiskPage.scss index eb752d819f..7bbc87f02f 100644 --- a/src/containers/VDiskPage/VDiskPage.scss +++ b/src/containers/VDiskPage/VDiskPage.scss @@ -30,6 +30,6 @@ } &__tablets-content { - margin-top: 16px; + margin-top: var(--g-spacing-4); } } diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss index 6776954888..e69de29bb2 100644 --- a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.scss @@ -1,9 +0,0 @@ -.ydb-vdisk-tablets { - &__error { - padding: 16px; - - text-align: center; - - color: var(--g-color-text-danger); - } -} diff --git a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx index 4ef0573675..5905113bfc 100644 --- a/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx +++ b/src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx @@ -1,29 +1,23 @@ import React from 'react'; -import type {Column} from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; import {skipToken} from '@reduxjs/toolkit/query'; +import {PageError} from '../../../components/Errors/PageError/PageError'; import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton'; import {ResizeableDataTable} from '../../../components/ResizeableDataTable/ResizeableDataTable'; import {vDiskApi} from '../../../store/reducers/vdisk/vdisk'; import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex'; -import {cn} from '../../../utils/cn'; +import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants'; import {useAutoRefreshInterval} from '../../../utils/hooks'; - -import {getColumns} from './columns'; +import {safeParseNumber} from '../../../utils/utils'; import {vDiskPageKeyset} from '../i18n'; -import './VDiskTablets.scss'; +import {getColumns} from './columns'; -const vDiskTabletsCn = cn('ydb-vdisk-tablets'); const VDISK_TABLETS_COLUMNS_WIDTH_LS_KEY = 'vdiskTabletsColumnsWidth'; -const TABLE_SETTINGS = { - displayIndices: false, - highlightRows: true, - stickyHead: DataTable.MOVING, -}; +const columns = getColumns(); interface VDiskTabletsProps { nodeId?: string | number; @@ -57,20 +51,20 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab // Transform the nested structure into flat table rows const flatData: VDiskBlobIndexItem[] = []; - stat.tablets.forEach((tablet: any) => { + stat.tablets.forEach((tablet) => { const tabletId = tablet.tablet_id; if (!tabletId || !Array.isArray(tablet.channels)) { return; // Skip tablets without ID or channels } - tablet.channels.forEach((channel: any, channelIndex: number) => { + tablet.channels.forEach((channel, channelIndex) => { // Only include channels that have count and data_size if (channel.count && channel.data_size) { flatData.push({ TabletId: tabletId, ChannelId: channelIndex, - Count: parseInt(channel.count, 10) || 0, - Size: parseInt(channel.data_size, 10) || 0, + Count: safeParseNumber(channel.count), + Size: safeParseNumber(channel.data_size), }); } }); @@ -79,14 +73,8 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab return flatData; }, [currentData]); - const columns = React.useMemo(() => getColumns(), []); - if (error) { - return ( -
- Error loading tablet statistics -
- ); + return ; } if (loading) { @@ -94,12 +82,12 @@ export function VDiskTablets({nodeId, pDiskId, vDiskSlotId, className}: VDiskTab } return ( -
+
[] { @@ -14,22 +17,29 @@ export function getColumns(): Column[] { render: ({row}) => { const tabletId = row.TabletId; if (!tabletId) { - return -; + return EMPTY_DATA_PLACEHOLDER; } - return {tabletId}; + return ( + {tabletId} + ); }, - width: 150, + width: 220, }, { name: vDiskPageKeyset('channel-id'), align: DataTable.RIGHT, - render: ({row}) => row.ChannelId ?? '-', - width: 100, + render: ({row}) => row.ChannelId ?? EMPTY_DATA_PLACEHOLDER, + width: 130, }, { name: vDiskPageKeyset('count'), align: DataTable.RIGHT, - render: ({row}) => row.Count ?? '-', + render: ({row}) => { + if (isNil(row.Count)) { + return EMPTY_DATA_PLACEHOLDER; + } + return formatNumber(row.Count); + }, width: 100, }, { @@ -37,7 +47,10 @@ export function getColumns(): Column[] { align: DataTable.RIGHT, render: ({row}) => { const size = row.Size; - const numericSize = Number(size) || 0; + if (isNil(size)) { + return EMPTY_DATA_PLACEHOLDER; + } + const numericSize = safeParseNumber(size); return formatBytes(numericSize); }, width: 120, From 0f9299a2e167fe847d70c267a60b4c7713655e8f Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 18 Jul 2025 09:34:48 +0300 Subject: [PATCH 8/8] fix: sorting --- .../VDiskPage/VDiskTablets/columns.tsx | 17 +++++++++++------ .../VDiskPage/VDiskTablets/constants.ts | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 6 deletions(-) create mode 100644 src/containers/VDiskPage/VDiskTablets/constants.ts diff --git a/src/containers/VDiskPage/VDiskTablets/columns.tsx b/src/containers/VDiskPage/VDiskTablets/columns.tsx index 09909bb2a1..bbc3e2fb4a 100644 --- a/src/containers/VDiskPage/VDiskTablets/columns.tsx +++ b/src/containers/VDiskPage/VDiskTablets/columns.tsx @@ -8,12 +8,14 @@ import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex'; import {EMPTY_DATA_PLACEHOLDER} from '../../../utils/constants'; import {formatBytes, formatNumber} from '../../../utils/dataFormatters/dataFormatters'; import {safeParseNumber} from '../../../utils/utils'; -import {vDiskPageKeyset} from '../i18n'; + +import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants'; export function getColumns(): Column[] { return [ { - name: vDiskPageKeyset('tablet-id'), + name: COLUMNS_NAMES.TABLET_ID, + header: COLUMNS_TITLES[COLUMNS_NAMES.TABLET_ID], render: ({row}) => { const tabletId = row.TabletId; if (!tabletId) { @@ -26,13 +28,16 @@ export function getColumns(): Column[] { width: 220, }, { - name: vDiskPageKeyset('channel-id'), + name: COLUMNS_NAMES.CHANNEL_ID, + header: COLUMNS_TITLES[COLUMNS_NAMES.CHANNEL_ID], align: DataTable.RIGHT, render: ({row}) => row.ChannelId ?? EMPTY_DATA_PLACEHOLDER, width: 130, + sortable: true, }, { - name: vDiskPageKeyset('count'), + name: COLUMNS_NAMES.COUNT, + header: COLUMNS_TITLES[COLUMNS_NAMES.COUNT], align: DataTable.RIGHT, render: ({row}) => { if (isNil(row.Count)) { @@ -43,7 +48,8 @@ export function getColumns(): Column[] { width: 100, }, { - name: vDiskPageKeyset('size'), + name: COLUMNS_NAMES.SIZE, + header: COLUMNS_TITLES[COLUMNS_NAMES.SIZE], align: DataTable.RIGHT, render: ({row}) => { const size = row.Size; @@ -54,7 +60,6 @@ export function getColumns(): Column[] { return formatBytes(numericSize); }, width: 120, - sortAccessor: (row) => row.Size || 0, }, ]; } diff --git a/src/containers/VDiskPage/VDiskTablets/constants.ts b/src/containers/VDiskPage/VDiskTablets/constants.ts new file mode 100644 index 0000000000..605a91be35 --- /dev/null +++ b/src/containers/VDiskPage/VDiskTablets/constants.ts @@ -0,0 +1,15 @@ +import {vDiskPageKeyset} from '../i18n'; + +export const COLUMNS_NAMES = { + TABLET_ID: 'TabletId', + CHANNEL_ID: 'ChannelId', + COUNT: 'Count', + SIZE: 'Size', +} as const; + +export const COLUMNS_TITLES = { + [COLUMNS_NAMES.TABLET_ID]: vDiskPageKeyset('tablet-id'), + [COLUMNS_NAMES.CHANNEL_ID]: vDiskPageKeyset('channel-id'), + [COLUMNS_NAMES.COUNT]: vDiskPageKeyset('count'), + [COLUMNS_NAMES.SIZE]: vDiskPageKeyset('size'), +} as const;