Skip to content

Commit 4222c14

Browse files
authored
feat: redesign Storage section (#2608)
1 parent c945a78 commit 4222c14

File tree

20 files changed

+234
-66
lines changed

20 files changed

+234
-66
lines changed

src/components/InfoViewer/InfoViewer.scss

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@use '../../styles/mixins.scss';
2+
13
.info-viewer {
24
--ydb-info-viewer-font-size: var(--g-text-body-2-font-size);
35
--ydb-info-viewer-line-height: var(--g-text-body-2-line-height);
@@ -88,4 +90,22 @@
8890
}
8991
}
9092
}
93+
94+
&_variant_small {
95+
.info-viewer__title {
96+
margin: 0 0 var(--g-spacing-3);
97+
98+
color: var(--g-color-text-primary);
99+
@include mixins.subheader-1-typography();
100+
}
101+
102+
.info-viewer__label {
103+
color: var(--g-color-text-secondary);
104+
@include mixins.body-1-typography();
105+
}
106+
107+
.info-viewer__row:first-child {
108+
padding-top: 0;
109+
}
110+
}
91111
}

src/components/InfoViewer/InfoViewer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface InfoViewerProps {
1616
info?: InfoViewerItem[];
1717
dots?: boolean;
1818
size?: 's';
19+
variant?: 'default' | 'small';
1920
className?: string;
2021
multilineLabels?: boolean;
2122
renderEmptyState?: (props?: Pick<InfoViewerProps, 'title' | 'size'>) => React.ReactNode;
@@ -28,6 +29,7 @@ export const InfoViewer = ({
2829
info,
2930
dots = true,
3031
size,
32+
variant = 'default',
3133
className,
3234
multilineLabels,
3335
renderEmptyState,
@@ -37,7 +39,7 @@ export const InfoViewer = ({
3739
}
3840

3941
return (
40-
<div className={b({size}, className)}>
42+
<div className={b({size, variant}, className)}>
4143
{title && <div className={b('title')}>{title}</div>}
4244
{info && info.length > 0 ? (
4345
<div className={b('items')}>

src/components/MemoryViewer/MemoryViewer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@ import type {TMemoryStats} from '../../types/api/nodes';
44
import {formatBytes} from '../../utils/bytesParsers';
55
import {cn} from '../../utils/cn';
66
import {GIGABYTE} from '../../utils/constants';
7+
import type {FormatProgressViewerValues} from '../../utils/progress';
78
import {calculateProgressStatus} from '../../utils/progress';
89
import {isNumeric} from '../../utils/utils';
910
import {HoverPopup} from '../HoverPopup/HoverPopup';
10-
import type {FormatProgressViewerValues} from '../ProgressViewer/ProgressViewer';
1111
import {ProgressViewer} from '../ProgressViewer/ProgressViewer';
1212

1313
import {calculateAllocatedMemory, getMemorySegments} from './utils';

src/components/ProgressViewer/ProgressViewer.tsx

Lines changed: 3 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import {useTheme} from '@gravity-ui/uikit';
22

33
import {cn} from '../../utils/cn';
4-
import {formatNumber, roundToPrecision} from '../../utils/dataFormatters/dataFormatters';
5-
import {calculateProgressStatus} from '../../utils/progress';
4+
import {calculateProgressStatus, defaultFormatProgressValues} from '../../utils/progress';
5+
import type {FormatProgressViewerValues} from '../../utils/progress';
66
import {isNumeric} from '../../utils/utils';
77

88
import './ProgressViewer.scss';
@@ -11,19 +11,6 @@ const b = cn('progress-viewer');
1111

1212
type ProgressViewerSize = 'xs' | 's' | 'ns' | 'm' | 'n' | 'l' | 'head';
1313

14-
export type FormatProgressViewerValues = (
15-
value?: number,
16-
capacity?: number,
17-
) => (string | number | undefined)[];
18-
19-
const formatValue = (value?: number) => {
20-
return formatNumber(roundToPrecision(Number(value), 2));
21-
};
22-
23-
const defaultFormatValues: FormatProgressViewerValues = (value, total) => {
24-
return [formatValue(value), formatValue(total)];
25-
};
26-
2714
/*
2815
2916
Props description:
@@ -56,7 +43,7 @@ export interface ProgressViewerProps {
5643
export function ProgressViewer({
5744
value,
5845
capacity,
59-
formatValues = defaultFormatValues,
46+
formatValues = defaultFormatProgressValues,
6047
percents,
6148
withOverflow,
6249
className,

src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/useTenantCpuQueryParams.ts

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import {StringParam, useQueryParams} from 'use-query-params';
22

33
import {
4-
TENANT_CPU_NODES_MODE_IDS,
5-
TENANT_CPU_TABS_IDS,
6-
} from '../../../../../store/reducers/tenant/constants';
4+
tenantCpuTabSchema,
5+
tenantNodesModeSchema,
6+
} from '../../../../../store/reducers/tenant/types';
77
import type {TenantCpuTab, TenantNodesMode} from '../../../../../store/reducers/tenant/types';
88

99
export function useTenantCpuQueryParams() {
@@ -12,27 +12,8 @@ export function useTenantCpuQueryParams() {
1212
nodesMode: StringParam,
1313
});
1414

15-
// Parse and validate cpuTab with fallback to nodes
16-
const cpuTab: TenantCpuTab = (() => {
17-
if (!queryParams.cpuTab) {
18-
return TENANT_CPU_TABS_IDS.nodes;
19-
}
20-
const validTabs = Object.values(TENANT_CPU_TABS_IDS) as string[];
21-
return validTabs.includes(queryParams.cpuTab)
22-
? (queryParams.cpuTab as TenantCpuTab)
23-
: TENANT_CPU_TABS_IDS.nodes;
24-
})();
25-
26-
// Parse and validate nodesMode with fallback to load
27-
const nodesMode: TenantNodesMode = (() => {
28-
if (!queryParams.nodesMode) {
29-
return TENANT_CPU_NODES_MODE_IDS.load;
30-
}
31-
const validModes = Object.values(TENANT_CPU_NODES_MODE_IDS) as string[];
32-
return validModes.includes(queryParams.nodesMode)
33-
? (queryParams.nodesMode as TenantNodesMode)
34-
: TENANT_CPU_NODES_MODE_IDS.load;
35-
})();
15+
const cpuTab: TenantCpuTab = tenantCpuTabSchema.parse(queryParams.cpuTab);
16+
const nodesMode: TenantNodesMode = tenantNodesModeSchema.parse(queryParams.nodesMode);
3617

3718
const handleCpuTabChange = (value: TenantCpuTab) => {
3819
setQueryParams({cpuTab: value}, 'replaceIn');

src/containers/Tenant/Diagnostics/TenantOverview/TenantDashboard/TenantDashboard.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.ydb-tenant-dashboard {
22
width: var(--diagnostics-section-table-width);
3-
margin-bottom: var(--diagnostics-section-margin);
3+
margin-bottom: var(--g-spacing-4);
44

55
&__charts {
66
display: flex;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React from 'react';
2+
3+
import {Flex, Progress, Text} from '@gravity-ui/uikit';
4+
5+
import {defaultFormatProgressValues} from '../../../../../utils/progress';
6+
import type {FormatProgressViewerValues} from '../../../../../utils/progress';
7+
import {isNumeric, safeParseNumber} from '../../../../../utils/utils';
8+
9+
import {DEFAULT_PROGRESS_WIDTH, MAX_PERCENTAGE, MIN_PERCENTAGE, PROGRESS_SIZE} from './constants';
10+
import i18n from './i18n';
11+
12+
interface ProgressWrapperProps {
13+
value?: number | string;
14+
capacity?: number | string;
15+
formatValues?: FormatProgressViewerValues;
16+
className?: string;
17+
width?: number;
18+
}
19+
20+
const isValidValue = (val?: number | string): boolean =>
21+
isNumeric(val) && safeParseNumber(val) >= 0;
22+
23+
export function ProgressWrapper({
24+
value,
25+
capacity,
26+
formatValues = defaultFormatProgressValues,
27+
className,
28+
width = DEFAULT_PROGRESS_WIDTH,
29+
}: ProgressWrapperProps) {
30+
if (!isValidValue(value)) {
31+
return <div className={className}>{i18n('alert_no-data')}</div>;
32+
}
33+
34+
const numericValue = safeParseNumber(value);
35+
const numericCapacity = safeParseNumber(capacity);
36+
37+
const rawPercentage =
38+
numericCapacity > 0
39+
? Math.floor((numericValue / numericCapacity) * MAX_PERCENTAGE)
40+
: MAX_PERCENTAGE;
41+
const fillWidth = Math.max(MIN_PERCENTAGE, rawPercentage);
42+
const clampedFillWidth = Math.min(fillWidth, MAX_PERCENTAGE);
43+
44+
const [valueText, capacityText] = React.useMemo(() => {
45+
return formatValues(Number(value), Number(capacity));
46+
}, [formatValues, value, capacity]);
47+
48+
const displayText = React.useMemo(() => {
49+
if (numericCapacity <= 0) {
50+
return String(valueText);
51+
}
52+
return i18n('context_capacity-usage', {value: valueText, capacity: capacityText});
53+
}, [valueText, capacityText, numericCapacity]);
54+
55+
const validatedWidth = Math.max(0, width);
56+
57+
return (
58+
<Flex alignItems="center" gap="2" className={className}>
59+
<div style={{width: `${validatedWidth}px`}}>
60+
<Progress value={clampedFillWidth} theme="success" size={PROGRESS_SIZE} />
61+
</div>
62+
<Text variant="body-1" color="secondary">
63+
{displayText}
64+
</Text>
65+
</Flex>
66+
);
67+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.tenant-storage {
2+
&__tabs-container {
3+
margin-top: var(--g-spacing-3);
4+
}
5+
6+
&__tab-content {
7+
margin-top: var(--g-spacing-3);
8+
}
9+
}

src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx

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

3+
import {Tab, TabList, TabProvider} from '@gravity-ui/uikit';
4+
35
import {InfoViewer} from '../../../../../components/InfoViewer/InfoViewer';
46
import {LabelWithPopover} from '../../../../../components/LabelWithPopover';
5-
import {ProgressViewer} from '../../../../../components/ProgressViewer/ProgressViewer';
7+
import {TENANT_STORAGE_TABS_IDS} from '../../../../../store/reducers/tenant/constants';
8+
import {cn} from '../../../../../utils/cn';
69
import {formatStorageValues} from '../../../../../utils/dataFormatters/dataFormatters';
710
import {TenantDashboard} from '../TenantDashboard/TenantDashboard';
811
import i18n from '../i18n';
9-
import {b} from '../utils';
1012

13+
import {ProgressWrapper} from './ProgressWrapper';
1114
import {TopGroups} from './TopGroups';
1215
import {TopTables} from './TopTables';
1316
import {storageDashboardConfig} from './storageDashboardConfig';
17+
import {useTenantStorageQueryParams} from './useTenantStorageQueryParams';
18+
19+
import './TenantStorage.scss';
1420

15-
import '../TenantOverview.scss';
21+
const tenantStorageCn = cn('tenant-storage');
22+
23+
const storageTabs = [
24+
{id: TENANT_STORAGE_TABS_IDS.tables, title: i18n('title_top-tables-by-size')},
25+
{id: TENANT_STORAGE_TABS_IDS.groups, title: i18n('title_top-groups-by-usage')},
26+
];
1627

1728
export interface TenantStorageMetrics {
1829
blobStorageUsed?: number;
@@ -27,8 +38,21 @@ interface TenantStorageProps {
2738
}
2839

2940
export function TenantStorage({tenantName, metrics}: TenantStorageProps) {
41+
const {storageTab, handleStorageTabChange} = useTenantStorageQueryParams();
42+
3043
const {blobStorageUsed, tabletStorageUsed, blobStorageLimit, tabletStorageLimit} = metrics;
3144

45+
const renderTabContent = () => {
46+
switch (storageTab) {
47+
case TENANT_STORAGE_TABS_IDS.tables:
48+
return <TopTables database={tenantName} />;
49+
case TENANT_STORAGE_TABS_IDS.groups:
50+
return <TopGroups tenant={tenantName} />;
51+
default:
52+
return null;
53+
}
54+
};
55+
3256
const info = [
3357
{
3458
label: (
@@ -38,11 +62,10 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) {
3862
/>
3963
),
4064
value: (
41-
<ProgressViewer
65+
<ProgressWrapper
4266
value={tabletStorageUsed}
4367
capacity={tabletStorageLimit}
4468
formatValues={formatStorageValues}
45-
colorizeProgress={true}
4669
/>
4770
),
4871
},
@@ -54,11 +77,10 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) {
5477
/>
5578
),
5679
value: (
57-
<ProgressViewer
80+
<ProgressWrapper
5881
value={blobStorageUsed}
5982
capacity={blobStorageLimit}
6083
formatValues={formatStorageValues}
61-
colorizeProgress={true}
6284
/>
6385
),
6486
},
@@ -67,9 +89,23 @@ export function TenantStorage({tenantName, metrics}: TenantStorageProps) {
6789
return (
6890
<React.Fragment>
6991
<TenantDashboard database={tenantName} charts={storageDashboardConfig} />
70-
<InfoViewer className={b('storage-info')} title="Storage details" info={info} />
71-
<TopTables database={tenantName} />
72-
<TopGroups tenant={tenantName} />
92+
<InfoViewer variant="small" title={i18n('title_storage-details')} info={info} />
93+
94+
<div className={tenantStorageCn('tabs-container')}>
95+
<TabProvider value={storageTab}>
96+
<TabList size="m">
97+
{storageTabs.map(({id, title}) => {
98+
return (
99+
<Tab key={id} value={id} onClick={() => handleStorageTabChange(id)}>
100+
{title}
101+
</Tab>
102+
);
103+
})}
104+
</TabList>
105+
</TabProvider>
106+
107+
<div className={tenantStorageCn('tab-content')}>{renderTabContent()}</div>
108+
</div>
73109
</React.Fragment>
74110
);
75111
}

src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ import {TENANT_OVERVIEW_TABLES_SETTINGS} from '../../../../../utils/constants';
1111
import {useAutoRefreshInterval} from '../../../../../utils/hooks';
1212
import {parseQueryErrorToString} from '../../../../../utils/query';
1313
import {TenantOverviewTableLayout} from '../TenantOverviewTableLayout';
14-
import {getSectionTitle} from '../getSectionTitle';
15-
import i18n from '../i18n';
1614

1715
import '../TenantOverview.scss';
1816

@@ -57,14 +55,8 @@ export function TopTables({database}: TopTablesProps) {
5755
) : null,
5856
},
5957
];
60-
const title = getSectionTitle({
61-
entity: i18n('tables'),
62-
postfix: i18n('by-size'),
63-
});
64-
6558
return (
6659
<TenantOverviewTableLayout
67-
title={title}
6860
loading={loading}
6961
error={parseQueryErrorToString(error)}
7062
withData={Boolean(currentData)}

0 commit comments

Comments
 (0)