Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
7 changes: 3 additions & 4 deletions src/containers/VDiskPage/VDiskPage.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
&__title,
&__controls,
&__info,
&__storage-title {
&__tabs {
position: sticky;
left: 0;

Expand All @@ -29,8 +29,7 @@
gap: var(--g-spacing-2);
}

&__storage-title {
margin-bottom: 0;
@include mixins.header-1-typography();
&__tablets-content {
margin-top: var(--g-spacing-4);
}
}
109 changes: 91 additions & 18 deletions src/containers/VDiskPage/VDiskPage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -25,26 +28,52 @@ 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();

const containerRef = React.useRef<HTMLDivElement>(null);
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]);
Expand Down Expand Up @@ -185,24 +214,67 @@ export function VDiskPage() {
return <VDiskInfo data={vDiskData} className={vDiskPageCn('info')} wrap />;
};

const renderTabs = () => {
const vDiskParamsDefined =
valueIsDefined(nodeId) && valueIsDefined(pDiskId) && valueIsDefined(vDiskSlotId);

return (
<div className={vDiskPageCn('tabs')}>
<TabProvider value={vDiskTab}>
<TabList size="l">
{VDISK_PAGE_TABS.map(({id, title}) => {
const path = vDiskParamsDefined
? getVDiskPagePath({nodeId, pDiskId, vDiskSlotId}, {activeTab: id})
: undefined;
return (
<Tab key={id} value={id}>
<InternalLink as="tab" to={path}>
{title}
</InternalLink>
</Tab>
);
})}
</TabList>
</TabProvider>
</div>
);
};

const renderTabsContent = () => {
switch (vDiskTab) {
case 'storage': {
return renderStorageInfo();
}
case 'tablets': {
return (
<VDiskTablets
nodeId={nodeId ?? undefined}
pDiskId={pDiskId ?? undefined}
vDiskSlotId={vDiskSlotId ?? undefined}
className={vDiskPageCn('tablets-content')}
/>
);
}
default:
return null;
}
};

const renderStorageInfo = () => {
if (valueIsDefined(GroupID) && valueIsDefined(nodeId)) {
return (
<React.Fragment>
<div className={vDiskPageCn('storage-title')}>{vDiskPageKeyset('storage')}</div>
<PaginatedStorage
groupId={GroupID}
nodeId={nodeId}
pDiskId={pDiskId ?? undefined}
scrollContainerRef={containerRef}
viewContext={{
groupId: GroupID?.toString(),
nodeId: nodeId?.toString(),
pDiskId: pDiskId?.toString(),
vDiskSlotId: vDiskSlotId?.toString(),
}}
/>
</React.Fragment>
<PaginatedStorage
groupId={GroupID}
nodeId={nodeId}
pDiskId={pDiskId ?? undefined}
scrollContainerRef={containerRef}
viewContext={{
groupId: GroupID?.toString(),
nodeId: nodeId?.toString(),
pDiskId: pDiskId?.toString(),
vDiskSlotId: vDiskSlotId?.toString(),
}}
/>
);
}

Expand All @@ -218,7 +290,8 @@ export function VDiskPage() {
<React.Fragment>
{error ? <ResponseError error={error} /> : null}
{renderInfo()}
{renderStorageInfo()}
{renderTabs()}
{renderTabsContent()}
</React.Fragment>
);
};
Expand Down
Empty file.
99 changes: 99 additions & 0 deletions src/containers/VDiskPage/VDiskTablets/VDiskTablets.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React from 'react';

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 {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
import {useAutoRefreshInterval} from '../../../utils/hooks';
import {safeParseNumber} from '../../../utils/utils';
import {vDiskPageKeyset} from '../i18n';

import {getColumns} from './columns';

const VDISK_TABLETS_COLUMNS_WIDTH_LS_KEY = 'vdiskTabletsColumnsWidth';

const columns = getColumns();

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[] = React.useMemo(() => {
if (!currentData) {
return [];
}

// Check if we have the expected structure: {stat: {tablets: [...]}}
const stat = currentData.stat;
if (!stat || !Array.isArray(stat.tablets)) {
return [];
}

// Transform the nested structure into flat table rows
const flatData: VDiskBlobIndexItem[] = [];

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, channelIndex) => {
// Only include channels that have count and data_size
if (channel.count && channel.data_size) {
flatData.push({
TabletId: tabletId,
ChannelId: channelIndex,
Count: safeParseNumber(channel.count),
Size: safeParseNumber(channel.data_size),
});
}
});
});

return flatData;
}, [currentData]);

if (error) {
return <PageError error={error} position="left" size="s" />;
}

if (loading) {
return <InfoViewerSkeleton rows={5} />;
}

return (
<div className={className}>
<ResizeableDataTable
columnsWidthLSKey={VDISK_TABLETS_COLUMNS_WIDTH_LS_KEY}
data={tableData}
columns={columns}
settings={DEFAULT_TABLE_SETTINGS}
loading={loading}
initialSortOrder={{
columnId: vDiskPageKeyset('size'),
order: DataTable.DESCENDING,
}}
/>
</div>
);
}
60 changes: 60 additions & 0 deletions src/containers/VDiskPage/VDiskTablets/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import type {Column} from '@gravity-ui/react-data-table';
import DataTable from '@gravity-ui/react-data-table';
import {isNil} from 'lodash';

import {InternalLink} from '../../../components/InternalLink/InternalLink';
import {getTabletPagePath} from '../../../routes';
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';

export function getColumns(): Column<VDiskBlobIndexItem>[] {
return [
{
name: vDiskPageKeyset('tablet-id'),
render: ({row}) => {
const tabletId = row.TabletId;
if (!tabletId) {
return EMPTY_DATA_PLACEHOLDER;
}
return (
<InternalLink to={getTabletPagePath(String(tabletId))}>{tabletId}</InternalLink>
);
},
width: 220,
},
{
name: vDiskPageKeyset('channel-id'),
align: DataTable.RIGHT,
render: ({row}) => row.ChannelId ?? EMPTY_DATA_PLACEHOLDER,
width: 130,
},
{
name: vDiskPageKeyset('count'),
align: DataTable.RIGHT,
render: ({row}) => {
if (isNil(row.Count)) {
return EMPTY_DATA_PLACEHOLDER;
}
return formatNumber(row.Count);
},
width: 100,
},
{
name: vDiskPageKeyset('size'),
align: DataTable.RIGHT,
render: ({row}) => {
const size = row.Size;
if (isNil(size)) {
return EMPTY_DATA_PLACEHOLDER;
}
const numericSize = safeParseNumber(size);
return formatBytes(numericSize);
},
width: 120,
sortAccessor: (row) => row.Size || 0,
},
];
}
1 change: 1 addition & 0 deletions src/containers/VDiskPage/VDiskTablets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {VDiskTablets} from './VDiskTablets';
6 changes: 6 additions & 0 deletions src/containers/VDiskPage/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Loading