Skip to content

Commit 460eed1

Browse files
authored
feat: implement colored state labels for storage groups (#4458)
1 parent 558de75 commit 460eed1

7 files changed

Lines changed: 198 additions & 17 deletions

File tree

‎src/components/DiskStatus/DiskStatus.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ export interface DiskStatusLabelData {
2828
dangerHeavy?: boolean;
2929
}
3030

31+
export type TitledDiskStatusLabelData = Omit<DiskStatusLabelData, 'title' | 'value'> & {
32+
title: string;
33+
value?: string;
34+
};
35+
3136
export function DiskStatusLabel({
3237
value,
3338
theme = 'normal',
@@ -37,7 +42,7 @@ export function DiskStatusLabel({
3742
dangerHeavy,
3843
size = 's',
3944
className,
40-
}: DiskStatusLabelData & Pick<LabelProps, 'size' | 'className'>) {
45+
}: (DiskStatusLabelData | TitledDiskStatusLabelData) & Pick<LabelProps, 'size' | 'className'>) {
4146
const label = (
4247
<Label
4348
size={size}

‎src/components/PDiskInfo/statuses.ts‎

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,7 @@ import {Ban, Check, CircleQuestionFill, CircleXmarkFill} from '@gravity-ui/icons
22

33
import type {EDecommitStatus, EDriveStatus, EMaintenanceStatus} from '../../types/api/pdisk';
44
import {TPDiskState} from '../../types/api/pdisk';
5-
import {
6-
NOT_AVAILABLE_SEVERITY,
7-
NUMERIC_SEVERITY_TO_LABEL_VIEW,
8-
SOLID_RED_SEVERITY,
9-
} from '../../utils/disks/constants';
5+
import {NOT_AVAILABLE_SEVERITY, NUMERIC_SEVERITY_TO_LABEL_VIEW} from '../../utils/disks/constants';
106
import {
117
getPDiskDecommitDisplayState,
128
getPDiskDriveDisplayState,
@@ -35,13 +31,12 @@ function getLabelView({
3531
DiskStatusLabelData,
3632
'theme' | 'icon' | 'dangerHeavy'
3733
> {
34+
const labelView = NUMERIC_SEVERITY_TO_LABEL_VIEW[severity];
35+
3836
return {
39-
theme:
40-
severity === NOT_AVAILABLE_SEVERITY
41-
? 'unknown'
42-
: NUMERIC_SEVERITY_TO_LABEL_VIEW[severity]?.theme,
37+
theme: severity === NOT_AVAILABLE_SEVERITY ? 'unknown' : labelView?.theme,
4338
icon,
44-
dangerHeavy: severity === SOLID_RED_SEVERITY,
39+
dangerHeavy: Boolean(labelView?.dangerHeavy),
4540
};
4641
}
4742

‎src/components/VDiskStatus/statuses.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {EVDiskDetailedReplicationStatus, EVDiskState} from '../../types/api/vdis
1111
import {
1212
NOT_AVAILABLE_SEVERITY,
1313
NUMERIC_SEVERITY_TO_LABEL_VIEW,
14-
SOLID_RED_SEVERITY,
1514
VDISK_STATE_SEVERITY_FOR_STATE_MODE,
1615
} from '../../utils/disks/constants';
1716
import {calculateStateIcon} from '../../utils/disks/iconCalculators';
@@ -62,11 +61,13 @@ export function getVDiskStateLabel(data: PreparedVDisk): DiskStatusLabelData {
6261
},
6362
};
6463

64+
const labelView = NUMERIC_SEVERITY_TO_LABEL_VIEW[severity];
65+
6566
return {
6667
...labels[state],
67-
theme: NUMERIC_SEVERITY_TO_LABEL_VIEW[severity].theme,
68+
theme: labelView.theme,
6869
icon: state === EVDiskState.OK ? Check : calculateStateIcon(data),
69-
dangerHeavy: severity === SOLID_RED_SEVERITY,
70+
dangerHeavy: Boolean(labelView.dangerHeavy),
7071
};
7172
}
7273

‎src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {Flex, Icon, Label, Popover} from '@gravity-ui/uikit';
66
import {isNil} from 'lodash';
77

88
import {CellWithPopover} from '../../../../components/CellWithPopover/CellWithPopover';
9+
import {DiskStatusLabel} from '../../../../components/DiskStatus/DiskStatus';
910
import {EntityName} from '../../../../components/EntityName/EntityName';
1011
import {StatusIcon} from '../../../../components/StatusIcon/StatusIcon';
1112
import {TitleWithHelpMark} from '../../../../components/TitleWithHelpmark/TitleWithHelpmark';
@@ -35,6 +36,7 @@ import {
3536
} from '../../Disks/constants';
3637
import {VDisks} from '../../VDisks/VDisks';
3738
import {getDegradedSeverity} from '../../utils';
39+
import {getStorageGroupStateLabel} from '../../utils/getStorageGroupStateLabel';
3840
import i18n from '../i18n';
3941

4042
import {
@@ -122,9 +124,14 @@ const degradedColumn: StorageGroupsColumn = {
122124
resizeMinWidth: 110,
123125
render: ({row}) =>
124126
row.Degraded ? (
125-
<Label theme={getDegradedSeverity(row)}>Degraded: {row.Degraded}</Label>
127+
<DiskStatusLabel
128+
size="xs"
129+
title="Degraded"
130+
value={String(row.Degraded)}
131+
theme={getDegradedSeverity(row)}
132+
/>
126133
) : (
127-
'-'
134+
EMPTY_DATA_PLACEHOLDER
128135
),
129136
align: DataTable.LEFT,
130137
defaultOrder: DataTable.DESCENDING,
@@ -133,7 +140,21 @@ const stateColumn: StorageGroupsColumn = {
133140
name: STORAGE_GROUPS_COLUMNS_IDS.State,
134141
header: STORAGE_GROUPS_COLUMNS_TITLES.State,
135142
width: 150,
136-
render: ({row}) => row.State ?? EMPTY_DATA_PLACEHOLDER,
143+
render: ({row}) => {
144+
const state = getStorageGroupStateLabel(row.State);
145+
146+
return state ? (
147+
<DiskStatusLabel
148+
size="xs"
149+
title={state.title}
150+
value={state.value}
151+
theme={state.theme}
152+
dangerHeavy={state.dangerHeavy}
153+
/>
154+
) : (
155+
EMPTY_DATA_PLACEHOLDER
156+
);
157+
},
137158
align: DataTable.LEFT,
138159
defaultOrder: DataTable.DESCENDING,
139160
};
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {getStorageGroupStateLabel} from '../getStorageGroupStateLabel';
2+
3+
describe('getStorageGroupStateLabel', () => {
4+
test.each([
5+
{state: 'ok', title: 'Ok', value: undefined, theme: 'success', dangerHeavy: false},
6+
{
7+
state: 'replicating',
8+
title: 'Replicating',
9+
value: undefined,
10+
theme: 'info',
11+
dangerHeavy: false,
12+
},
13+
{
14+
state: 'starting',
15+
title: 'Starting',
16+
value: undefined,
17+
theme: 'warning',
18+
dangerHeavy: false,
19+
},
20+
{state: 'dead', title: 'Dead', value: undefined, theme: 'danger', dangerHeavy: true},
21+
{
22+
state: 'replicating:3',
23+
title: 'Replicating',
24+
value: '3',
25+
theme: 'info',
26+
dangerHeavy: false,
27+
},
28+
{state: 'starting:2', title: 'Starting', value: '2', theme: 'warning', dangerHeavy: false},
29+
{state: 'degraded:1', title: 'Degraded', value: '1', theme: 'warning', dangerHeavy: false},
30+
{state: 'degraded:2', title: 'Degraded', value: '2', theme: 'danger', dangerHeavy: false},
31+
{state: 'dead:3', title: 'Dead', value: '3', theme: 'danger', dangerHeavy: true},
32+
{
33+
state: 'replicating:1(2)',
34+
title: 'Replicating',
35+
value: '1 (2)',
36+
theme: 'info',
37+
dangerHeavy: false,
38+
},
39+
{
40+
state: 'starting:1(3)',
41+
title: 'Starting',
42+
value: '1 (3)',
43+
theme: 'warning',
44+
dangerHeavy: false,
45+
},
46+
{
47+
state: 'degraded:1(3)',
48+
title: 'Degraded',
49+
value: '1 (3)',
50+
theme: 'warning',
51+
dangerHeavy: false,
52+
},
53+
{
54+
state: 'degraded:2(3,1)',
55+
title: 'Degraded',
56+
value: '2 (3,1)',
57+
theme: 'danger',
58+
dangerHeavy: false,
59+
},
60+
{
61+
state: 'dead:3(3,1,1)',
62+
title: 'Dead',
63+
value: '3 (3,1,1)',
64+
theme: 'danger',
65+
dangerHeavy: true,
66+
},
67+
])('formats $state with its expected appearance', ({state, ...expected}) => {
68+
expect(getStorageGroupStateLabel(state)).toEqual(expected);
69+
});
70+
71+
test.each([
72+
{state: 'degraded', title: 'Degraded', value: undefined},
73+
{state: 'degraded:0', title: 'Degraded', value: '0'},
74+
{state: 'degraded:3', title: 'Degraded', value: '3'},
75+
{state: 'degraded:10', title: 'Degraded', value: '10'},
76+
{state: 'degraded:20', title: 'Degraded', value: '20'},
77+
{state: 'degraded:3(3,1,1)', title: 'Degraded', value: '3 (3,1,1)'},
78+
{state: 'degraded:unknown', title: 'Degraded', value: 'unknown'},
79+
])('keeps $state visibly degraded when the count is unsupported', ({state, ...expected}) => {
80+
expect(getStorageGroupStateLabel(state)).toEqual({
81+
...expected,
82+
theme: 'warning',
83+
dangerHeavy: false,
84+
});
85+
});
86+
87+
test.each([undefined, null, '', ' \n\t '])('returns no label for %p', (state) => {
88+
expect(getStorageGroupStateLabel(state)).toBeUndefined();
89+
});
90+
91+
test('normalizes surrounding whitespace and spacing before the details', () => {
92+
expect(getStorageGroupStateLabel(' degraded: 2 (3, 1) ')).toEqual({
93+
title: 'Degraded',
94+
value: '2 (3, 1)',
95+
theme: 'danger',
96+
dangerHeavy: false,
97+
});
98+
});
99+
100+
test('preserves an unknown status and its details with a neutral appearance', () => {
101+
expect(getStorageGroupStateLabel('future-state:7(2,1)')).toEqual({
102+
title: 'Future-state',
103+
value: '7 (2,1)',
104+
theme: 'normal',
105+
dangerHeavy: false,
106+
});
107+
});
108+
});
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type {TitledDiskStatusLabelData} from '../../../components/DiskStatus/DiskStatus';
2+
import {
3+
DATA_SEVERITY,
4+
NUMERIC_SEVERITY_TO_LABEL_VIEW,
5+
SOLID_RED_SEVERITY,
6+
} from '../../../utils/disks/constants';
7+
import type {DisplaySeverity} from '../../../utils/disks/types';
8+
9+
const stateSeverities = new Map<string, DisplaySeverity>([
10+
['ok', DATA_SEVERITY.GREEN],
11+
['replicating', DATA_SEVERITY.BLUE],
12+
['starting', DATA_SEVERITY.YELLOW],
13+
['degraded', DATA_SEVERITY.YELLOW],
14+
['dead', SOLID_RED_SEVERITY],
15+
]);
16+
17+
export function getStorageGroupStateLabel(
18+
state?: string | null,
19+
): TitledDiskStatusLabelData | undefined {
20+
const normalizedState = state?.trim();
21+
22+
if (!normalizedState) {
23+
return undefined;
24+
}
25+
26+
const match = normalizedState.match(/^([^:]+)(?::\s*(.+))?$/);
27+
const status = (match?.[1] ?? normalizedState).trim();
28+
const value = match?.[2]?.trim();
29+
let severity = stateSeverities.get(status) ?? DATA_SEVERITY.GREY;
30+
31+
if (status === 'degraded') {
32+
const count = value?.split('(', 1)[0].trim();
33+
34+
if (count === '2') {
35+
severity = DATA_SEVERITY.RED;
36+
}
37+
}
38+
39+
const labelView = NUMERIC_SEVERITY_TO_LABEL_VIEW[severity];
40+
const title = status[0].toUpperCase() + status.slice(1);
41+
const formattedValue = value?.replace(/\s*\(/g, ' (');
42+
43+
return {
44+
title,
45+
value: formattedValue,
46+
theme: labelView.theme,
47+
dangerHeavy: Boolean(labelView.dangerHeavy),
48+
};
49+
}

‎src/utils/disks/constants.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ export const PDISK_STATE_SEVERITY = {
142142
export interface LabelVisualConfig {
143143
theme: LabelProps['theme'];
144144
icon?: IconData;
145+
dangerHeavy?: boolean;
145146
}
146147

147148
export const NUMERIC_SEVERITY_TO_LABEL_VIEW: Record<number, LabelVisualConfig> = {
@@ -159,6 +160,7 @@ export const NUMERIC_SEVERITY_TO_LABEL_VIEW: Record<number, LabelVisualConfig> =
159160
},
160161
[SOLID_RED_SEVERITY]: {
161162
theme: 'danger',
163+
dangerHeavy: true,
162164
},
163165
[DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue]: {
164166
theme: 'info',

0 commit comments

Comments
 (0)