ydb-embedded-ui 4.18.0 → 4.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/components/FullNodeViewer/FullNodeViewer.scss +1 -1
  3. package/dist/components/FullNodeViewer/FullNodeViewer.tsx +13 -4
  4. package/dist/components/ProgressViewer/ProgressViewer.tsx +11 -5
  5. package/dist/containers/Nodes/getNodesColumns.tsx +1 -0
  6. package/dist/containers/Storage/PDiskPopup/PDiskPopup.tsx +1 -1
  7. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +3 -232
  8. package/dist/containers/Storage/StorageGroups/getStorageGroupsColumns.tsx +278 -0
  9. package/dist/containers/Tenant/Diagnostics/TenantOverview/MetricsCards/MetricsCards.tsx +2 -3
  10. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.tsx +22 -6
  11. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.scss +41 -0
  12. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +68 -0
  13. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopGroups.tsx +76 -0
  14. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TopTables.tsx +105 -0
  15. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/en.json +2 -0
  16. package/dist/containers/Tenant/Diagnostics/TenantOverview/i18n/ru.json +2 -0
  17. package/dist/containers/Versions/NodesTable/NodesTable.tsx +1 -0
  18. package/dist/store/reducers/index.ts +4 -0
  19. package/dist/store/reducers/storage/utils.ts +20 -11
  20. package/dist/store/reducers/tenantOverview/executeTopTables/executeTopTables.ts +93 -0
  21. package/dist/store/reducers/tenantOverview/executeTopTables/types.ts +14 -0
  22. package/dist/store/reducers/tenantOverview/topStorageGroups/topStorageGroups.ts +98 -0
  23. package/dist/store/reducers/tenantOverview/topStorageGroups/types.ts +29 -0
  24. package/dist/store/reducers/tenantOverview/topStorageGroups/utils.ts +20 -0
  25. package/dist/store/reducers/tenants/utils.ts +28 -18
  26. package/dist/styles/constants.scss +4 -0
  27. package/dist/types/additionalProps.ts +1 -1
  28. package/dist/types/api/storage.ts +1 -1
  29. package/dist/types/api/tenant.ts +21 -8
  30. package/dist/utils/bytesParsers/__test__/formatBytes.test.ts +10 -1
  31. package/dist/utils/bytesParsers/formatBytes.ts +3 -3
  32. package/dist/utils/constants.ts +8 -0
  33. package/dist/utils/dataFormatters/__test__/roundToSignificant.test.ts +22 -0
  34. package/dist/utils/dataFormatters/dataFormatters.ts +46 -20
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.19.1](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.19.0...v4.19.1) (2023-10-11)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * add storage value to tb formatter ([#547](https://github.com/ydb-platform/ydb-embedded-ui/issues/547)) ([f1e4377](https://github.com/ydb-platform/ydb-embedded-ui/commit/f1e4377443be493a7072aca33a62b51e381f6841))
9
+
10
+ ## [4.19.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.18.0...v4.19.0) (2023-10-11)
11
+
12
+
13
+ ### Features
14
+
15
+ * **TenantOverview:** add storage tab to tenant diagnostics ([#541](https://github.com/ydb-platform/ydb-embedded-ui/issues/541)) ([c4cdd35](https://github.com/ydb-platform/ydb-embedded-ui/commit/c4cdd354cd9780dfd7dfee80ec225f59d4230625))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * add NodeId to NodeAddress type ([#545](https://github.com/ydb-platform/ydb-embedded-ui/issues/545)) ([3df82d3](https://github.com/ydb-platform/ydb-embedded-ui/commit/3df82d39466696ec61e34b915b355dacd0482ebc))
21
+ * display database name in node info ([#543](https://github.com/ydb-platform/ydb-embedded-ui/issues/543)) ([788ad9a](https://github.com/ydb-platform/ydb-embedded-ui/commit/788ad9a7a1a56ffe93ec7e4861ded6cceef72d9c))
22
+ * fix cpu usage calculation ([#542](https://github.com/ydb-platform/ydb-embedded-ui/issues/542)) ([f46b03d](https://github.com/ydb-platform/ydb-embedded-ui/commit/f46b03d6157f19017560d71a9ab6591f045bad96))
23
+ * fix incorrect data display in ProgressViewer ([#546](https://github.com/ydb-platform/ydb-embedded-ui/issues/546)) ([be077b8](https://github.com/ydb-platform/ydb-embedded-ui/commit/be077b83a4b4cf083d506e77abf0f2b6570c87d3))
24
+
3
25
  ## [4.18.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.17.0...v4.18.0) (2023-09-25)
4
26
 
5
27
 
@@ -23,7 +23,7 @@
23
23
  }
24
24
 
25
25
  .info-viewer__label {
26
- min-width: 60px;
26
+ min-width: 100px;
27
27
  }
28
28
 
29
29
  &__section-title {
@@ -5,7 +5,7 @@ import type {TSystemStateInfo} from '../../types/api/nodes';
5
5
  import {LOAD_AVERAGE_TIME_INTERVALS} from '../../utils/constants';
6
6
  import {calcUptime} from '../../utils/dataFormatters/dataFormatters';
7
7
 
8
- import InfoViewer from '../InfoViewer/InfoViewer';
8
+ import InfoViewer, {type InfoViewerItem} from '../InfoViewer/InfoViewer';
9
9
  import {ProgressViewer} from '../ProgressViewer/ProgressViewer';
10
10
  import {PoolUsage} from '../PoolUsage/PoolUsage';
11
11
 
@@ -24,16 +24,25 @@ export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => {
24
24
  value: Address,
25
25
  }));
26
26
 
27
- const commonInfo = [
27
+ const commonInfo: InfoViewerItem[] = [];
28
+
29
+ // Do not add DB field for static nodes (they have no Tenants)
30
+ if (node?.Tenants?.length) {
31
+ commonInfo.push({label: 'Database', value: node.Tenants[0]});
32
+ }
33
+
34
+ commonInfo.push(
28
35
  {label: 'Version', value: node?.Version},
29
36
  {label: 'Uptime', value: calcUptime(node?.StartTime)},
30
37
  {label: 'DC', value: node?.DataCenterDescription},
31
38
  {label: 'Rack', value: node?.Rack},
32
- ];
39
+ );
33
40
 
34
41
  const averageInfo = node?.LoadAverage?.map((load, loadIndex) => ({
35
42
  label: LOAD_AVERAGE_TIME_INTERVALS[loadIndex],
36
- value: <ProgressViewer value={load} percents={true} colorizeProgress={true} />,
43
+ value: (
44
+ <ProgressViewer value={load} percents={true} colorizeProgress={true} capacity={100} />
45
+ ),
37
46
  }));
38
47
 
39
48
  return (
@@ -46,7 +46,7 @@ interface ProgressViewerProps {
46
46
 
47
47
  export function ProgressViewer({
48
48
  value,
49
- capacity = 100,
49
+ capacity,
50
50
  formatValues,
51
51
  percents,
52
52
  className,
@@ -84,13 +84,19 @@ export function ProgressViewer({
84
84
 
85
85
  const text = fillWidth > 60 ? 'contrast0' : 'contrast70';
86
86
 
87
- if (!isNaN(fillWidth)) {
87
+ const renderContent = () => {
88
+ if (capacityText) {
89
+ return `${valueText} ${divider} ${capacityText}`;
90
+ }
91
+
92
+ return valueText;
93
+ };
94
+
95
+ if (!isNaN(Number(value))) {
88
96
  return (
89
97
  <div className={b({size}, className)}>
90
98
  <div className={b('line', {bg})} style={lineStyle}></div>
91
- <span
92
- className={b('text', {text})}
93
- >{`${valueText} ${divider} ${capacityText}`}</span>
99
+ <span className={b('text', {text})}>{renderContent()}</span>
94
100
  </div>
95
101
  );
96
102
  }
@@ -120,6 +120,7 @@ export function getNodesColumns({
120
120
  row.LoadAverage && row.LoadAverage.length > 0 ? (
121
121
  <ProgressViewer
122
122
  value={row.LoadAverage[0]}
123
+ capacity={100}
123
124
  percents={true}
124
125
  colorizeProgress={true}
125
126
  />
@@ -23,7 +23,7 @@ export const preparePDiskData = (data: TPDiskStateInfo, nodes?: NodesMap) => {
23
23
  const {AvailableSize, TotalSize, State, PDiskId, NodeId, Path, Realtime, Device} = data;
24
24
 
25
25
  const pdiskData: InfoViewerItem[] = [
26
- {label: 'PDisk', value: getPDiskId({NodeId, PDiskId})},
26
+ {label: 'PDisk', value: getPDiskId({NodeId, PDiskId}) || '-'},
27
27
  {label: 'State', value: State || 'not available'},
28
28
  {label: 'Type', value: getPDiskType(data) || 'unknown'},
29
29
  ];
@@ -1,25 +1,12 @@
1
- import cn from 'bem-cn-lite';
2
-
3
1
  import DataTable, {Column, Settings, SortOrder} from '@gravity-ui/react-data-table';
4
- import {Icon, Label, Popover, PopoverBehavior} from '@gravity-ui/uikit';
5
2
 
6
- import type {ValueOf} from '../../../types/common';
7
3
  import type {NodesMap} from '../../../types/store/nodesList';
8
4
  import type {PreparedStorageGroup, VisibleEntities} from '../../../store/reducers/storage/types';
9
5
  import type {HandleSort} from '../../../utils/hooks/useTableSort';
10
-
11
6
  import {VISIBLE_ENTITIES} from '../../../store/reducers/storage/constants';
12
- import {bytesToGB, bytesToSpeed} from '../../../utils/utils';
13
- import {stringifyVdiskId} from '../../../utils/dataFormatters/dataFormatters';
14
- import {getUsage, isFullVDiskData, isSortableStorageProperty} from '../../../utils/storage';
15
-
16
- import shieldIcon from '../../../assets/icons/shield.svg';
17
- import {Stack} from '../../../components/Stack/Stack';
18
- import EntityStatus from '../../../components/EntityStatus/EntityStatus';
19
-
7
+ import {isSortableStorageProperty} from '../../../utils/storage';
20
8
  import {EmptyFilter} from '../EmptyFilter/EmptyFilter';
21
- import {VDisk} from '../VDisk';
22
- import {getDegradedSeverity, getUsageSeverityForStorageGroup} from '../utils';
9
+ import {getStorageGroupsColumns} from './getStorageGroupsColumns';
23
10
 
24
11
  import i18n from './i18n';
25
12
  import './StorageGroups.scss';
@@ -39,8 +26,6 @@ const TableColumnsIds = {
39
26
  Degraded: 'Degraded',
40
27
  } as const;
41
28
 
42
- type TableColumnId = ValueOf<typeof TableColumnsIds>;
43
-
44
29
  interface StorageGroupsProps {
45
30
  data: PreparedStorageGroup[];
46
31
  nodes?: NodesMap;
@@ -51,23 +36,6 @@ interface StorageGroupsProps {
51
36
  handleSort?: HandleSort;
52
37
  }
53
38
 
54
- const tableColumnsNames: Record<TableColumnId, string> = {
55
- PoolName: 'Pool Name',
56
- Kind: 'Type',
57
- Erasure: 'Erasure',
58
- GroupId: 'Group ID',
59
- Used: 'Used',
60
- Limit: 'Limit',
61
- UsedSpaceFlag: 'Space',
62
- Usage: 'Usage',
63
- Read: 'Read',
64
- Write: 'Write',
65
- VDisks: 'VDisks',
66
- Degraded: 'Degraded',
67
- };
68
-
69
- const b = cn('global-storage-groups');
70
-
71
39
  export function StorageGroups({
72
40
  data,
73
41
  tableSettings,
@@ -77,204 +45,7 @@ export function StorageGroups({
77
45
  sort,
78
46
  handleSort,
79
47
  }: StorageGroupsProps) {
80
- const rawColumns: Column<PreparedStorageGroup>[] = [
81
- {
82
- name: TableColumnsIds.PoolName,
83
- header: tableColumnsNames[TableColumnsIds.PoolName],
84
- width: 250,
85
- render: ({row}) => {
86
- const splitted = row.PoolName?.split('/');
87
- return (
88
- <div className={b('pool-name-wrapper')}>
89
- {splitted && (
90
- <Popover
91
- content={row.PoolName}
92
- placement={['right']}
93
- behavior={PopoverBehavior.Immediate}
94
- >
95
- <span className={b('pool-name')}>
96
- {splitted[splitted.length - 1]}
97
- </span>
98
- </Popover>
99
- )}
100
- </div>
101
- );
102
- },
103
- align: DataTable.LEFT,
104
- },
105
- {
106
- name: TableColumnsIds.Kind,
107
- header: tableColumnsNames[TableColumnsIds.Kind],
108
- // prettier-ignore
109
- render: ({row}) => (
110
- <>
111
- <Label>{row.Kind || '—'}</Label>
112
- {' '}
113
- {row.Encryption && (
114
- <Popover
115
- content={i18n('encrypted')}
116
- placement="right"
117
- behavior={PopoverBehavior.Immediate}
118
- >
119
- <Label>
120
- <Icon data={shieldIcon} />
121
- </Label>
122
- </Popover>
123
- )}
124
- </>
125
- ),
126
- },
127
- {
128
- name: TableColumnsIds.Erasure,
129
- header: tableColumnsNames[TableColumnsIds.Erasure],
130
- render: ({row}) => (row.ErasureSpecies ? row.ErasureSpecies : '-'),
131
- align: DataTable.LEFT,
132
- },
133
- {
134
- name: TableColumnsIds.Degraded,
135
- header: tableColumnsNames[TableColumnsIds.Degraded],
136
- width: 100,
137
- render: ({row}) =>
138
- row.Degraded ? (
139
- <Label theme={getDegradedSeverity(row)}>Degraded: {row.Degraded}</Label>
140
- ) : (
141
- '-'
142
- ),
143
- align: DataTable.LEFT,
144
- defaultOrder: DataTable.DESCENDING,
145
- },
146
- {
147
- name: TableColumnsIds.Usage,
148
- header: tableColumnsNames[TableColumnsIds.Usage],
149
- width: 100,
150
- render: ({row}) => {
151
- // without a limit the usage can be evaluated as 0,
152
- // but the absence of a value is more clear
153
- return row.Limit ? (
154
- <Label
155
- theme={getUsageSeverityForStorageGroup(row.Usage)}
156
- className={b('usage-label', {overload: row.Usage >= 90})}
157
- >
158
- {row.Usage}%
159
- </Label>
160
- ) : (
161
- '-'
162
- );
163
- },
164
- // without a limit exclude usage from sort to display at the bottom
165
- sortAccessor: (row) => (row.Limit ? getUsage(row) : null),
166
- align: DataTable.LEFT,
167
- },
168
- {
169
- name: TableColumnsIds.GroupId,
170
- header: tableColumnsNames[TableColumnsIds.GroupId],
171
- width: 130,
172
- render: ({row}) => {
173
- return <span className={b('group-id')}>{row.GroupID}</span>;
174
- },
175
- sortAccessor: (row) => Number(row.GroupID),
176
- align: DataTable.RIGHT,
177
- },
178
- {
179
- name: TableColumnsIds.Used,
180
- header: tableColumnsNames[TableColumnsIds.Used],
181
- width: 100,
182
- render: ({row}) => {
183
- return bytesToGB(row.Used, true);
184
- },
185
- align: DataTable.RIGHT,
186
- },
187
- {
188
- name: TableColumnsIds.Limit,
189
- header: tableColumnsNames[TableColumnsIds.Limit],
190
- width: 100,
191
- render: ({row}) => {
192
- return bytesToGB(row.Limit);
193
- },
194
- align: DataTable.RIGHT,
195
- },
196
- {
197
- name: TableColumnsIds.UsedSpaceFlag,
198
- header: tableColumnsNames[TableColumnsIds.UsedSpaceFlag],
199
- width: 110,
200
- render: ({row}) => {
201
- const value = row.UsedSpaceFlag;
202
-
203
- let color = 'Red';
204
-
205
- if (value < 100) {
206
- color = 'Green';
207
- } else if (value < 10000) {
208
- color = 'Yellow';
209
- } else if (value < 1000000) {
210
- color = 'Orange';
211
- }
212
- return <EntityStatus status={color} />;
213
- },
214
- align: DataTable.CENTER,
215
- },
216
-
217
- {
218
- name: TableColumnsIds.Read,
219
- header: tableColumnsNames[TableColumnsIds.Read],
220
- width: 100,
221
- render: ({row}) => {
222
- return row.Read ? bytesToSpeed(row.Read) : '-';
223
- },
224
- align: DataTable.RIGHT,
225
- },
226
- {
227
- name: TableColumnsIds.Write,
228
- header: tableColumnsNames[TableColumnsIds.Write],
229
- width: 100,
230
- render: ({row}) => {
231
- return row.Write ? bytesToSpeed(row.Write) : '-';
232
- },
233
- align: DataTable.RIGHT,
234
- },
235
- {
236
- name: TableColumnsIds.VDisks,
237
- className: b('vdisks-column'),
238
- header: tableColumnsNames[TableColumnsIds.VDisks],
239
- render: ({row}) => (
240
- <div className={b('vdisks-wrapper')}>
241
- {row.VDisks?.map((vDisk) => {
242
- const donors = vDisk.Donors;
243
-
244
- return donors && donors.length > 0 ? (
245
- <Stack
246
- className={b('vdisks-item')}
247
- key={stringifyVdiskId(vDisk.VDiskId)}
248
- >
249
- <VDisk data={vDisk} nodes={nodes} />
250
- {donors.map((donor) => {
251
- const isFullData = isFullVDiskData(donor);
252
-
253
- return (
254
- <VDisk
255
- data={isFullData ? donor : {...donor, DonorMode: true}}
256
- // donor and acceptor are always in the same group
257
- nodes={nodes}
258
- key={stringifyVdiskId(
259
- isFullData ? donor.VDiskId : donor,
260
- )}
261
- />
262
- );
263
- })}
264
- </Stack>
265
- ) : (
266
- <div className={b('vdisks-item')} key={stringifyVdiskId(vDisk.VDiskId)}>
267
- <VDisk data={vDisk} nodes={nodes} />
268
- </div>
269
- );
270
- })}
271
- </div>
272
- ),
273
- align: DataTable.CENTER,
274
- sortable: false,
275
- width: 900,
276
- },
277
- ];
48
+ const rawColumns: Column<PreparedStorageGroup>[] = getStorageGroupsColumns(nodes);
278
49
 
279
50
  let columns = rawColumns.map((column) => ({
280
51
  ...column,
@@ -0,0 +1,278 @@
1
+ import cn from 'bem-cn-lite';
2
+
3
+ import DataTable, {type Column} from '@gravity-ui/react-data-table';
4
+ import {Icon, Label, Popover, PopoverBehavior} from '@gravity-ui/uikit';
5
+
6
+ import shieldIcon from '../../../assets/icons/shield.svg';
7
+ import type {ValueOf} from '../../../types/common';
8
+ import type {NodesMap} from '../../../types/store/nodesList';
9
+ import type {PreparedStorageGroup} from '../../../store/reducers/storage/types';
10
+ import {getUsage, isFullVDiskData} from '../../../utils/storage';
11
+ import {bytesToGB, bytesToSpeed} from '../../../utils/utils';
12
+ import {stringifyVdiskId} from '../../../utils/dataFormatters/dataFormatters';
13
+ import EntityStatus from '../../../components/EntityStatus/EntityStatus';
14
+ import {Stack} from '../../../components/Stack/Stack';
15
+ import {VDisk} from '../VDisk';
16
+ import {getDegradedSeverity, getUsageSeverityForStorageGroup} from '../utils';
17
+ import i18n from './i18n';
18
+ import './StorageGroups.scss';
19
+
20
+ const b = cn('global-storage-groups');
21
+
22
+ const GROUPS_COLUMNS_IDS = {
23
+ PoolName: 'PoolName',
24
+ Kind: 'Kind',
25
+ Erasure: 'Erasure',
26
+ GroupId: 'GroupId',
27
+ Used: 'Used',
28
+ Limit: 'Limit',
29
+ Usage: 'Usage',
30
+ UsedSpaceFlag: 'UsedSpaceFlag',
31
+ Read: 'Read',
32
+ Write: 'Write',
33
+ VDisks: 'VDisks',
34
+ Degraded: 'Degraded',
35
+ } as const;
36
+
37
+ type GroupsColumns = ValueOf<typeof GROUPS_COLUMNS_IDS>;
38
+
39
+ const tableColumnsNames: Record<GroupsColumns, string> = {
40
+ PoolName: 'Pool Name',
41
+ Kind: 'Type',
42
+ Erasure: 'Erasure',
43
+ GroupId: 'Group ID',
44
+ Used: 'Used',
45
+ Limit: 'Limit',
46
+ UsedSpaceFlag: 'Space',
47
+ Usage: 'Usage',
48
+ Read: 'Read',
49
+ Write: 'Write',
50
+ VDisks: 'VDisks',
51
+ Degraded: 'Degraded',
52
+ };
53
+
54
+ const poolNameColumn: Column<PreparedStorageGroup> = {
55
+ name: GROUPS_COLUMNS_IDS.PoolName,
56
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.PoolName],
57
+ width: 250,
58
+ render: ({row}) => {
59
+ const splitted = row.PoolName?.split('/');
60
+ return (
61
+ <div className={b('pool-name-wrapper')}>
62
+ {splitted && (
63
+ <Popover
64
+ content={row.PoolName}
65
+ placement={['right']}
66
+ behavior={PopoverBehavior.Immediate}
67
+ >
68
+ <span className={b('pool-name')}>{splitted[splitted.length - 1]}</span>
69
+ </Popover>
70
+ )}
71
+ </div>
72
+ );
73
+ },
74
+ align: DataTable.LEFT,
75
+ };
76
+
77
+ const kindColumn: Column<PreparedStorageGroup> = {
78
+ name: GROUPS_COLUMNS_IDS.Kind,
79
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Kind],
80
+ // prettier-ignore
81
+ render: ({row}) => (
82
+ <>
83
+ <Label>{row.Kind || '—'}</Label>
84
+ {' '}
85
+ {row.Encryption && (
86
+ <Popover
87
+ content={i18n('encrypted')}
88
+ placement="right"
89
+ behavior={PopoverBehavior.Immediate}
90
+ >
91
+ <Label>
92
+ <Icon data={shieldIcon} />
93
+ </Label>
94
+ </Popover>
95
+ )}
96
+ </>
97
+ ),
98
+ sortable: false,
99
+ };
100
+
101
+ const erasureColumn: Column<PreparedStorageGroup> = {
102
+ name: GROUPS_COLUMNS_IDS.Erasure,
103
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Erasure],
104
+ render: ({row}) => (row.ErasureSpecies ? row.ErasureSpecies : '-'),
105
+ align: DataTable.LEFT,
106
+ sortable: false,
107
+ };
108
+
109
+ const degradedColumn: Column<PreparedStorageGroup> = {
110
+ name: GROUPS_COLUMNS_IDS.Degraded,
111
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Degraded],
112
+ width: 100,
113
+ render: ({row}) =>
114
+ row.Degraded ? (
115
+ <Label theme={getDegradedSeverity(row)}>Degraded: {row.Degraded}</Label>
116
+ ) : (
117
+ '-'
118
+ ),
119
+ align: DataTable.LEFT,
120
+ defaultOrder: DataTable.DESCENDING,
121
+ };
122
+
123
+ const usageColumn: Column<PreparedStorageGroup> = {
124
+ name: GROUPS_COLUMNS_IDS.Usage,
125
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Usage],
126
+ width: 100,
127
+ render: ({row}) => {
128
+ // without a limit the usage can be evaluated as 0,
129
+ // but the absence of a value is more clear
130
+ return row.Limit ? (
131
+ <Label
132
+ theme={getUsageSeverityForStorageGroup(row.Usage)}
133
+ className={b('usage-label', {overload: row.Usage >= 90})}
134
+ >
135
+ {row.Usage}%
136
+ </Label>
137
+ ) : (
138
+ '-'
139
+ );
140
+ },
141
+ // without a limit exclude usage from sort to display at the bottom
142
+ sortAccessor: (row) => (row.Limit ? getUsage(row) : null),
143
+ align: DataTable.LEFT,
144
+ sortable: false,
145
+ };
146
+
147
+ const groupIdColumn: Column<PreparedStorageGroup> = {
148
+ name: GROUPS_COLUMNS_IDS.GroupId,
149
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.GroupId],
150
+ width: 130,
151
+ render: ({row}) => {
152
+ return <span className={b('group-id')}>{row.GroupID}</span>;
153
+ },
154
+ sortAccessor: (row) => Number(row.GroupID),
155
+ align: DataTable.RIGHT,
156
+ sortable: false,
157
+ };
158
+
159
+ const usedColumn: Column<PreparedStorageGroup> = {
160
+ name: GROUPS_COLUMNS_IDS.Used,
161
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Used],
162
+ width: 100,
163
+ render: ({row}) => {
164
+ return bytesToGB(row.Used, true);
165
+ },
166
+ align: DataTable.RIGHT,
167
+ sortable: false,
168
+ };
169
+
170
+ const limitColumn: Column<PreparedStorageGroup> = {
171
+ name: GROUPS_COLUMNS_IDS.Limit,
172
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Limit],
173
+ width: 100,
174
+ render: ({row}) => {
175
+ return bytesToGB(row.Limit);
176
+ },
177
+ align: DataTable.RIGHT,
178
+ sortable: false,
179
+ };
180
+
181
+ const usedSpaceFlagColumn: Column<PreparedStorageGroup> = {
182
+ name: GROUPS_COLUMNS_IDS.UsedSpaceFlag,
183
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.UsedSpaceFlag],
184
+ width: 110,
185
+ render: ({row}) => {
186
+ const value = row.UsedSpaceFlag;
187
+
188
+ let color = 'Red';
189
+
190
+ if (value < 100) {
191
+ color = 'Green';
192
+ } else if (value < 10000) {
193
+ color = 'Yellow';
194
+ } else if (value < 1000000) {
195
+ color = 'Orange';
196
+ }
197
+ return <EntityStatus status={color} />;
198
+ },
199
+ align: DataTable.CENTER,
200
+ };
201
+
202
+ const readColumn: Column<PreparedStorageGroup> = {
203
+ name: GROUPS_COLUMNS_IDS.Read,
204
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Read],
205
+ width: 100,
206
+ render: ({row}) => {
207
+ return row.Read ? bytesToSpeed(row.Read) : '-';
208
+ },
209
+ align: DataTable.RIGHT,
210
+ };
211
+
212
+ const writeColumn: Column<PreparedStorageGroup> = {
213
+ name: GROUPS_COLUMNS_IDS.Write,
214
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.Write],
215
+ width: 100,
216
+ render: ({row}) => {
217
+ return row.Write ? bytesToSpeed(row.Write) : '-';
218
+ },
219
+ align: DataTable.RIGHT,
220
+ };
221
+
222
+ const getVdiscksColumn = (nodes?: NodesMap): Column<PreparedStorageGroup> => ({
223
+ name: GROUPS_COLUMNS_IDS.VDisks,
224
+ className: b('vdisks-column'),
225
+ header: tableColumnsNames[GROUPS_COLUMNS_IDS.VDisks],
226
+ render: ({row}) => (
227
+ <div className={b('vdisks-wrapper')}>
228
+ {row.VDisks?.map((vDisk) => {
229
+ const donors = vDisk.Donors;
230
+
231
+ return donors && donors.length > 0 ? (
232
+ <Stack className={b('vdisks-item')} key={stringifyVdiskId(vDisk.VDiskId)}>
233
+ <VDisk data={vDisk} nodes={nodes} />
234
+ {donors.map((donor) => {
235
+ const isFullData = isFullVDiskData(donor);
236
+
237
+ return (
238
+ <VDisk
239
+ data={isFullData ? donor : {...donor, DonorMode: true}}
240
+ // donor and acceptor are always in the same group
241
+ nodes={nodes}
242
+ key={stringifyVdiskId(isFullData ? donor.VDiskId : donor)}
243
+ />
244
+ );
245
+ })}
246
+ </Stack>
247
+ ) : (
248
+ <div className={b('vdisks-item')} key={stringifyVdiskId(vDisk.VDiskId)}>
249
+ <VDisk data={vDisk} nodes={nodes} />
250
+ </div>
251
+ );
252
+ })}
253
+ </div>
254
+ ),
255
+ align: DataTable.CENTER,
256
+ width: 900,
257
+ });
258
+
259
+ export const getStorageTopGroupsColumns = (): Column<PreparedStorageGroup>[] => {
260
+ return [groupIdColumn, kindColumn, erasureColumn, usageColumn, usedColumn, limitColumn];
261
+ };
262
+
263
+ export const getStorageGroupsColumns = (nodes?: NodesMap): Column<PreparedStorageGroup>[] => {
264
+ return [
265
+ poolNameColumn,
266
+ kindColumn,
267
+ erasureColumn,
268
+ degradedColumn,
269
+ usageColumn,
270
+ groupIdColumn,
271
+ usedColumn,
272
+ limitColumn,
273
+ usedSpaceFlagColumn,
274
+ readColumn,
275
+ writeColumn,
276
+ getVdiscksColumn(nodes),
277
+ ];
278
+ };