ydb-embedded-ui 1.10.3 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (28) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/components/IndexInfoViewer/IndexInfoViewer.tsx +2 -2
  3. package/dist/components/InfoViewer/InfoViewer.scss +32 -7
  4. package/dist/components/InfoViewer/InfoViewer.tsx +43 -0
  5. package/dist/components/InfoViewer/index.ts +1 -0
  6. package/dist/components/InfoViewer/utils.ts +6 -4
  7. package/dist/components/Stack/Stack.scss +55 -0
  8. package/dist/components/Stack/Stack.tsx +35 -0
  9. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.scss +2 -0
  10. package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +5 -0
  11. package/dist/containers/Storage/Pdisk/Pdisk.scss +2 -19
  12. package/dist/containers/Storage/Pdisk/Pdisk.tsx +30 -33
  13. package/dist/containers/Storage/Pdisk/__tests__/colors.tsx +40 -0
  14. package/dist/containers/Storage/StorageGroups/StorageGroups.scss +31 -3
  15. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +72 -33
  16. package/dist/containers/Storage/Vdisk/Vdisk.js +63 -64
  17. package/dist/containers/Storage/Vdisk/Vdisk.scss +9 -28
  18. package/dist/containers/Storage/Vdisk/__tests__/colors.tsx +163 -0
  19. package/dist/containers/Storage/utils/index.ts +49 -0
  20. package/dist/services/api.d.ts +9 -0
  21. package/dist/setupTests.js +8 -0
  22. package/dist/types/api/schema.ts +6 -14
  23. package/dist/types/api/storage.ts +164 -0
  24. package/dist/types/index.ts +1 -0
  25. package/dist/types/store/storage.ts +11 -0
  26. package/package.json +28 -5
  27. package/dist/components/InfoViewer/InfoViewer.js +0 -47
  28. package/dist/index.test.js +0 -5
@@ -1,11 +1,13 @@
1
1
  import _ from 'lodash';
2
2
  import cn from 'bem-cn-lite';
3
3
  import DataTable, {Column, Settings, SortOrder} from '@yandex-cloud/react-data-table';
4
- import {Popover, PopoverBehavior} from '@yandex-cloud/uikit';
4
+ import {Label, Popover, PopoverBehavior} from '@yandex-cloud/uikit';
5
5
 
6
- import Vdisk from '../Vdisk/Vdisk';
6
+ import {Stack} from '../../../components/Stack/Stack';
7
7
  //@ts-ignore
8
8
  import EntityStatus from '../../../components/EntityStatus/EntityStatus';
9
+
10
+ import {TVDiskStateInfo} from '../../../types/api/storage';
9
11
  //@ts-ignore
10
12
  import {VisibleEntities} from '../../../store/reducers/storage';
11
13
  //@ts-ignore
@@ -13,6 +15,9 @@ import {bytesToGB, bytesToSpeed} from '../../../utils/utils';
13
15
  //@ts-ignore
14
16
  import {stringifyVdiskId} from '../../../utils';
15
17
 
18
+ import Vdisk from '../Vdisk/Vdisk';
19
+ import {isFullDonorData, getDegradedSeverity, getUsageSeverity, getUsage} from '../utils';
20
+
16
21
  import './StorageGroups.scss';
17
22
 
18
23
  enum TableColumnsIds {
@@ -44,11 +49,11 @@ const tableColumnsNames: Record<TableColumnsIdsValues, string> = {
44
49
  Used: 'Used',
45
50
  Limit: 'Limit',
46
51
  UsedSpaceFlag: 'Space',
47
- UsedPercents: 'Used percents',
52
+ UsedPercents: 'Usage',
48
53
  Read: 'Read',
49
54
  Write: 'Write',
50
55
  VDisks: 'VDisks',
51
- Missing: 'Missing',
56
+ Missing: 'Degraded',
52
57
  };
53
58
 
54
59
  const b = cn('global-storage-groups');
@@ -57,8 +62,8 @@ function setSortOrder(visibleEntities: keyof typeof VisibleEntities): SortOrder
57
62
  switch (visibleEntities) {
58
63
  case VisibleEntities.All: {
59
64
  return {
60
- columnId: TableColumnsIds.GroupID,
61
- order: DataTable.ASCENDING,
65
+ columnId: TableColumnsIds.Missing,
66
+ order: DataTable.DESCENDING,
62
67
  };
63
68
  }
64
69
  case VisibleEntities.Missing: {
@@ -105,6 +110,34 @@ function StorageGroups({data, tableSettings, visibleEntities, nodes}: StorageGro
105
110
  },
106
111
  align: DataTable.LEFT,
107
112
  },
113
+ {
114
+ name: TableColumnsIds.Missing,
115
+ header: tableColumnsNames[TableColumnsIds.Missing],
116
+ width: 100,
117
+ render: ({value, row}) => value ? (
118
+ <Label theme={getDegradedSeverity(row)}>Degraded: {value}</Label>
119
+ ) : '-',
120
+ align: DataTable.LEFT,
121
+ defaultOrder: DataTable.DESCENDING,
122
+ },
123
+ {
124
+ name: TableColumnsIds.UsedPercents,
125
+ header: tableColumnsNames[TableColumnsIds.UsedPercents],
126
+ width: 100,
127
+ render: ({row}) => {
128
+ const usage = getUsage(row, 5);
129
+ return (
130
+ <Label
131
+ theme={getUsageSeverity(usage)}
132
+ className={b('usage-label', {overload: usage >= 100})}
133
+ >
134
+ ≥ {usage}%
135
+ </Label>
136
+ );
137
+ },
138
+ sortAccessor: getUsage,
139
+ align: DataTable.LEFT,
140
+ },
108
141
  {
109
142
  name: TableColumnsIds.GroupID,
110
143
  header: tableColumnsNames[TableColumnsIds.GroupID],
@@ -132,18 +165,6 @@ function StorageGroups({data, tableSettings, visibleEntities, nodes}: StorageGro
132
165
  },
133
166
  align: DataTable.RIGHT,
134
167
  },
135
- // {
136
- // name: tableColumnsIds.UsedPercents,
137
- // header: tableColumnsNames[tableColumnsIds.UsedPercents],
138
- // width: '100px',
139
- // render: ({row}) => {
140
- // return (
141
- // Math.round((row[tableColumnsIds.Used] * 100) / row[tableColumnsIds.Limit]) +
142
- // '%'
143
- // );
144
- // },
145
- // align: DataTable.RIGHT,
146
- // },
147
168
  {
148
169
  name: TableColumnsIds.UsedSpaceFlag,
149
170
  header: tableColumnsNames[TableColumnsIds.UsedSpaceFlag],
@@ -181,26 +202,44 @@ function StorageGroups({data, tableSettings, visibleEntities, nodes}: StorageGro
181
202
  },
182
203
  align: DataTable.RIGHT,
183
204
  },
184
- {
185
- name: TableColumnsIds.Missing,
186
- header: tableColumnsNames[TableColumnsIds.Missing],
187
- width: 100,
188
- align: DataTable.CENTER,
189
- defaultOrder: DataTable.DESCENDING,
190
- },
191
205
  {
192
206
  name: TableColumnsIds.VDisks,
207
+ className: b('vdisks-column'),
193
208
  header: tableColumnsNames[TableColumnsIds.VDisks],
194
209
  render: ({value, row}) => (
195
210
  <div className={b('vdisks-wrapper')}>
196
- {_.map(value as any, (el) => (
197
- <Vdisk
198
- key={stringifyVdiskId(el.VDiskId)}
199
- {...el}
200
- PoolName={row[TableColumnsIds.PoolName]}
201
- nodes={nodes}
202
- />
203
- ))}
211
+ {_.map(value as TVDiskStateInfo[], (el) => {
212
+ const donors = Array.isArray(el.Donors) ? el.Donors.filter(isFullDonorData) : [];
213
+
214
+ return (
215
+ donors.length > 0 ? (
216
+ <Stack className={b('vdisks-item')} key={stringifyVdiskId(el.VDiskId)}>
217
+ <Vdisk
218
+ {...el}
219
+ PoolName={row[TableColumnsIds.PoolName]}
220
+ nodes={nodes}
221
+ />
222
+ {donors.map((donor) => (
223
+ <Vdisk
224
+ {...donor}
225
+ // donor and acceptor are always in the same group
226
+ PoolName={row[TableColumnsIds.PoolName]}
227
+ nodes={nodes}
228
+ key={stringifyVdiskId(donor.VDiskId)}
229
+ />
230
+ ))}
231
+ </Stack>
232
+ ) : (
233
+ <div className={b('vdisks-item')} key={stringifyVdiskId(el.VDiskId)}>
234
+ <Vdisk
235
+ {...el}
236
+ PoolName={row[TableColumnsIds.PoolName]}
237
+ nodes={nodes}
238
+ />
239
+ </div>
240
+ )
241
+ );
242
+ })}
204
243
  </div>
205
244
  ),
206
245
  align: DataTable.CENTER,
@@ -1,13 +1,13 @@
1
1
  import React, {useEffect, useState, useRef, useMemo} from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import cn from 'bem-cn-lite';
4
- import _ from 'lodash';
5
- import {Popup} from '@yandex-cloud/uikit';
4
+ import {Label, Popup} from '@yandex-cloud/uikit';
6
5
 
7
6
  import {bytesToGB, bytesToSpeed} from '../../../utils/utils';
8
7
  import routes, {createHref} from '../../../routes';
9
8
  import {stringifyVdiskId, getPDiskId} from '../../../utils';
10
9
  import {getPDiskType} from '../../../utils/pdisk';
10
+ import {InfoViewer} from '../../../components/InfoViewer';
11
11
  import DiskStateProgressBar, {
12
12
  diskProgressColors,
13
13
  } from '../DiskStateProgressBar/DiskStateProgressBar';
@@ -26,6 +26,9 @@ const propTypes = {
26
26
  FrontQueues: PropTypes.string,
27
27
  Replicated: PropTypes.bool,
28
28
  PoolName: PropTypes.string,
29
+ VDiskId: PropTypes.object,
30
+ DonorMode: PropTypes.bool,
31
+ nodes: PropTypes.object,
29
32
  };
30
33
 
31
34
  const stateSeverity = {
@@ -53,7 +56,7 @@ function Vdisk(props) {
53
56
 
54
57
  // determine disk status severity
55
58
  useEffect(() => {
56
- const {DiskSpace, VDiskState, FrontQueues, Replicated} = props;
59
+ const {DiskSpace, VDiskState, FrontQueues, Replicated, DonorMode} = props;
57
60
 
58
61
  // if the disk is not available, this determines its status severity regardless of other features
59
62
  if (!VDiskState) {
@@ -66,7 +69,10 @@ function Vdisk(props) {
66
69
  const FrontQueuesSeverity = Math.min(colorSeverity.Orange, getColorSeverity(FrontQueues));
67
70
 
68
71
  let newSeverity = Math.max(DiskSpaceSeverity, VDiskSpaceSeverity, FrontQueuesSeverity);
69
- if (!Replicated && newSeverity === colorSeverity.Green) {
72
+
73
+ // donors are always in the not replicated state since they are leftovers
74
+ // painting them blue is useless
75
+ if (!Replicated && !DonorMode && newSeverity === colorSeverity.Green) {
70
76
  newSeverity = colorSeverity.Blue;
71
77
  }
72
78
 
@@ -95,62 +101,62 @@ function Vdisk(props) {
95
101
  ReadThroughput,
96
102
  WriteThroughput,
97
103
  } = props;
98
- const vdiskData = [{property: 'VDisk', value: stringifyVdiskId(VDiskId)}];
99
- vdiskData.push({property: 'State', value: VDiskState ?? 'not available'});
100
- PoolName && vdiskData.push({property: 'StoragePool', value: PoolName});
104
+ const vdiskData = [{label: 'VDisk', value: stringifyVdiskId(VDiskId)}];
105
+ vdiskData.push({label: 'State', value: VDiskState ?? 'not available'});
106
+ PoolName && vdiskData.push({label: 'StoragePool', value: PoolName});
101
107
 
102
108
  SatisfactionRank &&
103
109
  SatisfactionRank.FreshRank?.Flag !== diskProgressColors[colorSeverity.Green] &&
104
110
  vdiskData.push({
105
- property: 'Fresh',
111
+ label: 'Fresh',
106
112
  value: SatisfactionRank.FreshRank.Flag,
107
113
  });
108
114
 
109
115
  SatisfactionRank &&
110
116
  SatisfactionRank.LevelRank?.Flag !== diskProgressColors[colorSeverity.Green] &&
111
117
  vdiskData.push({
112
- property: 'Level',
118
+ label: 'Level',
113
119
  value: SatisfactionRank.LevelRank.Flag,
114
120
  });
115
121
 
116
122
  SatisfactionRank &&
117
123
  SatisfactionRank.FreshRank?.RankPercent &&
118
124
  vdiskData.push({
119
- property: 'Fresh',
125
+ label: 'Fresh',
120
126
  value: SatisfactionRank.FreshRank.RankPercent,
121
127
  });
122
128
 
123
129
  SatisfactionRank &&
124
130
  SatisfactionRank.LevelRank?.RankPercent &&
125
131
  vdiskData.push({
126
- property: 'Level',
132
+ label: 'Level',
127
133
  value: SatisfactionRank.LevelRank.RankPercent,
128
134
  });
129
135
 
130
136
  DiskSpace &&
131
137
  DiskSpace !== diskProgressColors[colorSeverity.Green] &&
132
- vdiskData.push({property: 'Space', value: DiskSpace});
138
+ vdiskData.push({label: 'Space', value: DiskSpace});
133
139
 
134
140
  FrontQueues &&
135
141
  FrontQueues !== diskProgressColors[colorSeverity.Green] &&
136
- vdiskData.push({property: 'FrontQueues', value: FrontQueues});
142
+ vdiskData.push({label: 'FrontQueues', value: FrontQueues});
137
143
 
138
- !Replicated && vdiskData.push({property: 'Replicated', value: 'NO'});
144
+ !Replicated && vdiskData.push({label: 'Replicated', value: 'NO'});
139
145
 
140
- UnsyncedVDisks && vdiskData.push({property: 'UnsyncVDisks', value: UnsyncedVDisks});
146
+ UnsyncedVDisks && vdiskData.push({label: 'UnsyncVDisks', value: UnsyncedVDisks});
141
147
 
142
148
  Boolean(Number(AllocatedSize)) &&
143
149
  vdiskData.push({
144
- property: 'Allocated',
150
+ label: 'Allocated',
145
151
  value: bytesToGB(AllocatedSize),
146
152
  });
147
153
 
148
154
  Boolean(Number(ReadThroughput)) &&
149
- vdiskData.push({property: 'Read', value: bytesToSpeed(ReadThroughput)});
155
+ vdiskData.push({label: 'Read', value: bytesToSpeed(ReadThroughput)});
150
156
 
151
157
  Boolean(Number(WriteThroughput)) &&
152
158
  vdiskData.push({
153
- property: 'Write',
159
+ label: 'Write',
154
160
  value: bytesToSpeed(WriteThroughput),
155
161
  });
156
162
 
@@ -165,61 +171,54 @@ function Vdisk(props) {
165
171
  diskProgressColors[colorSeverity.Yellow],
166
172
  ];
167
173
  if (PDisk && nodes) {
168
- const pdiskData = [{property: 'PDisk', value: getPDiskId(PDisk)}];
174
+ const pdiskData = [{label: 'PDisk', value: getPDiskId(PDisk)}];
169
175
  pdiskData.push({
170
- property: 'State',
176
+ label: 'State',
171
177
  value: PDisk.State || 'not available',
172
178
  });
173
- pdiskData.push({property: 'Type', value: getPDiskType(PDisk) || 'unknown'});
174
- PDisk.NodeId && pdiskData.push({property: 'Node Id', value: PDisk.NodeId});
179
+ pdiskData.push({label: 'Type', value: getPDiskType(PDisk) || 'unknown'});
180
+ PDisk.NodeId && pdiskData.push({label: 'Node Id', value: PDisk.NodeId});
175
181
  PDisk.NodeId &&
176
182
  nodes[PDisk.NodeId] &&
177
- pdiskData.push({property: 'Host', value: nodes[PDisk.NodeId]});
178
- PDisk.Path && pdiskData.push({property: 'Path', value: PDisk.Path});
183
+ pdiskData.push({label: 'Host', value: nodes[PDisk.NodeId]});
184
+ PDisk.Path && pdiskData.push({label: 'Path', value: PDisk.Path});
179
185
  pdiskData.push({
180
- property: 'Available',
186
+ label: 'Available',
181
187
  value: `${bytesToGB(PDisk.AvailableSize)} of ${bytesToGB(PDisk.TotalSize)}`,
182
188
  });
183
189
  errorColors.includes(PDisk.Realtime) &&
184
- pdiskData.push({property: 'Realtime', value: PDisk.Realtime});
190
+ pdiskData.push({label: 'Realtime', value: PDisk.Realtime});
185
191
  errorColors.includes(PDisk.Device) &&
186
- pdiskData.push({property: 'Device', value: PDisk.Device});
192
+ pdiskData.push({label: 'Device', value: PDisk.Device});
187
193
  return pdiskData;
188
194
  }
189
195
  return null;
190
196
  };
191
197
  /* eslint-enable */
192
198
 
193
- const renderPopup = () => {
194
- const vdiskData = prepareVdiskData();
195
- const pdiskData = preparePdiskData();
196
- return (
197
- <Popup
198
- className={b('popup-wrapper')}
199
- anchorRef={anchor}
200
- open={isPopupVisible}
201
- placement={['top', 'bottom']}
202
- hasArrow
203
- >
204
- <div className={b('popup-content')}>
205
- <div className={b('popup-section-name')}>VDisk</div>
206
- {_.map(vdiskData, (row) => (
207
- <React.Fragment key={row.property}>
208
- <div className={b('property')}>{row.property}</div>
209
- <div className={b('value')}>{row.value}</div>
210
- </React.Fragment>
211
- ))}
212
- <div className={b('popup-section-name')}>PDisk</div>
213
- {_.map(pdiskData, (row) => (
214
- <React.Fragment key={row.property}>
215
- <div className={b('property')}>{row.property}</div>
216
- <div className={b('value')}>{row.value}</div>
217
- </React.Fragment>
218
- ))}
219
- </div>
220
- </Popup>
221
- );
222
- };
199
+ const renderPopup = () => (
200
+ <Popup
201
+ className={b('popup-wrapper')}
202
+ anchorRef={anchor}
203
+ open={isPopupVisible}
204
+ placement={['top', 'bottom']}
205
+ // bigger offset for easier switching to neighbour nodes
206
+ // matches the default offset for popup with arrow out of a sense of beauty
207
+ offset={[0, 12]}
208
+ >
209
+ {props.DonorMode && <Label className={b('donor-label')}>Donor</Label>}
210
+ <InfoViewer
211
+ title="VDisk"
212
+ info={prepareVdiskData()}
213
+ size="s"
214
+ />
215
+ <InfoViewer
216
+ title="PDisk"
217
+ info={preparePdiskData()}
218
+ size="s"
219
+ />
220
+ </Popup>
221
+ );
223
222
 
224
223
  const vdiskAllocatedPercent = useMemo(() => {
225
224
  const {AvailableSize, AllocatedSize, PDisk} = props;
@@ -243,13 +242,13 @@ function Vdisk(props) {
243
242
  href={
244
243
  props.NodeId
245
244
  ? createHref(
246
- routes.node,
247
- {id: props.NodeId, activeTab: STRUCTURE},
248
- {
249
- pdiskId: props.PDisk?.PDiskId,
250
- vdiskId: stringifyVdiskId(props.VDiskId),
251
- },
252
- )
245
+ routes.node,
246
+ {id: props.NodeId, activeTab: STRUCTURE},
247
+ {
248
+ pdiskId: props.PDisk?.PDiskId,
249
+ vdiskId: stringifyVdiskId(props.VDiskId),
250
+ },
251
+ )
253
252
  : undefined
254
253
  }
255
254
  />
@@ -1,35 +1,16 @@
1
1
  .vdisk-storage {
2
- display: flex;
3
- flex-grow: 1;
4
- align-items: center;
5
-
6
- max-width: 200px;
7
- margin-right: 10px;
8
-
9
- cursor: pointer;
10
-
11
- &:last-child {
12
- margin-right: 0px;
13
- }
14
2
  &__popup-wrapper {
15
- padding: 5px 10px;
16
- }
17
- &__popup-content {
18
- display: grid;
19
- justify-items: stretch;
20
- column-gap: 5px;
21
- }
22
- &__popup-section-name {
23
- grid-column: 1 / 3;
3
+ padding: 12px;
24
4
 
25
- margin: 5px 0;
5
+ .info-viewer + .info-viewer {
6
+ margin-top: 8px;
7
+ padding-top: 8px;
26
8
 
27
- font-weight: 500;
28
- text-align: center;
29
-
30
- border-bottom: 1px solid var(--yc-color-line-generic);
9
+ border-top: 1px solid var(--yc-color-line-generic);
10
+ }
31
11
  }
32
- &__property {
33
- text-align: right;
12
+
13
+ &__donor-label {
14
+ margin-bottom: 8px;
34
15
  }
35
16
  }
@@ -0,0 +1,163 @@
1
+ import {render} from '@testing-library/react'
2
+
3
+ import VDisk from '../Vdisk'
4
+
5
+ describe('VDisk state', () => {
6
+ it('Should determine severity based on the highest value among VDiskState, DiskSpace and FrontQueues', () => {
7
+ const {getAllByRole} = render(
8
+ <>
9
+ <VDisk
10
+ VDiskId={{Domain: 1}}
11
+ VDiskState="OK" // severity 1, green
12
+ DiskSpace="Yellow" // severity 3, yellow
13
+ FrontQueues="Green" // severity 1, green
14
+ />
15
+ <VDisk
16
+ VDiskId={{Domain: 2}}
17
+ VDiskState="PDiskError" // severity 5, red
18
+ DiskSpace="Yellow" // severity 3, yellow
19
+ FrontQueues="Green" // severity 1, green
20
+ />
21
+ <VDisk
22
+ VDiskId={{Domain: 3}}
23
+ VDiskState="OK" // severity 1, green
24
+ DiskSpace="Yellow" // severity 3, yellow
25
+ FrontQueues="Orange" // severity 4, orange
26
+ />
27
+ </>
28
+ );
29
+
30
+ const [disk1, disk2, disk3] = getAllByRole('meter');
31
+
32
+ expect(disk1.className).toMatch(/_yellow\b/i);
33
+ expect(disk2.className).toMatch(/_red\b/i);
34
+ expect(disk3.className).toMatch(/_orange\b/i);
35
+ });
36
+
37
+ it('Should not pick the highest severity based on FrontQueues value', () => {
38
+ const {getAllByRole} = render(
39
+ <>
40
+ <VDisk
41
+ VDiskId={{Domain: 1}}
42
+ VDiskState="OK" // severity 1, green
43
+ DiskSpace="Green" // severity 1, green
44
+ FrontQueues="Red" // severity 5, red
45
+ />
46
+ <VDisk
47
+ VDiskId={{Domain: 2}}
48
+ VDiskState="OK" // severity 1, green
49
+ DiskSpace="Red" // severity 5, red
50
+ FrontQueues="Red" // severity 5, red
51
+ />
52
+ </>
53
+ );
54
+
55
+ const [disk1, disk2] = getAllByRole('meter');
56
+
57
+ expect(disk1.className).not.toMatch(/_red\b/i);
58
+ expect(disk2.className).toMatch(/_red\b/i);
59
+ });
60
+
61
+ it('Should display as unavailable when no VDiskState is provided', () => {
62
+ const {getAllByRole} = render(
63
+ <>
64
+ <VDisk VDiskId={{Domain: 1}} />
65
+ <VDisk VDiskId={{Domain: 2}} VDiskState="OK" />
66
+ <VDisk VDiskId={{Domain: 3}} DiskSpace="Green" />
67
+ <VDisk VDiskId={{Domain: 4}} FrontQueues="Green" />
68
+ <VDisk VDiskId={{Domain: 5}} VDiskState="OK" DiskSpace="Green" />
69
+ <VDisk VDiskId={{Domain: 6}} VDiskState="OK" FrontQueues="Green" />
70
+ <VDisk VDiskId={{Domain: 7}} DiskSpace="Green" FrontQueues="Green" />
71
+ <VDisk VDiskId={{Domain: 8}} VDiskState="OK" DiskSpace="Green" FrontQueues="Green" />
72
+ </>
73
+ );
74
+
75
+ const [disk1, disk2, disk3, disk4, disk5, disk6, disk7, disk8] = getAllByRole('meter');
76
+
77
+ // unavailable disks display with the highest severity
78
+ expect(disk1.className).toMatch(/_red\b/i);
79
+ expect(disk2.className).not.toMatch(/_red\b/i);
80
+ expect(disk3.className).toMatch(/_red\b/i);
81
+ expect(disk4.className).toMatch(/_red\b/i);
82
+ expect(disk5.className).not.toMatch(/_red\b/i);
83
+ expect(disk6.className).not.toMatch(/_red\b/i);
84
+ expect(disk7.className).toMatch(/_red\b/i);
85
+ expect(disk8.className).not.toMatch(/_red\b/i);
86
+ });
87
+
88
+ it('Should display replicating VDisks in OK state with a distinct color', () => {
89
+ const {getAllByRole} = render(
90
+ <>
91
+ <VDisk
92
+ VDiskId={{Domain: 1}}
93
+ VDiskState="OK" // severity 1, green
94
+ Replicated={false}
95
+ />
96
+ <VDisk
97
+ VDiskId={{Domain: 2}}
98
+ VDiskState="OK" // severity 1, green
99
+ Replicated={true}
100
+ />
101
+ </>
102
+ );
103
+
104
+ const [disk1, disk2] = getAllByRole('meter');
105
+
106
+ expect(disk1.className).toMatch(/_blue\b/i);
107
+ expect(disk2.className).not.toMatch(/_blue\b/i);
108
+ });
109
+
110
+ it('Should display replicating VDisks in a not-OK state with a regular color', () => {
111
+ const {getAllByRole} = render(
112
+ <>
113
+ <VDisk
114
+ VDiskId={{Domain: 1}}
115
+ VDiskState="Initial" // severity 3, yellow
116
+ Replicated={false}
117
+ />
118
+ <VDisk
119
+ VDiskId={{Domain: 2}}
120
+ VDiskState="PDiskError" // severity 5, red
121
+ Replicated={false}
122
+ />
123
+ </>
124
+ );
125
+
126
+ const [disk1, disk2] = getAllByRole('meter');
127
+
128
+ expect(disk1.className).toMatch(/_yellow\b/i);
129
+ expect(disk2.className).toMatch(/_red\b/i);
130
+ });
131
+
132
+ it('Should always display donor VDisks with a regular color', () => {
133
+ const {getAllByRole} = render(
134
+ <>
135
+ <VDisk
136
+ VDiskId={{Domain: 1}}
137
+ VDiskState="OK" // severity 1, green
138
+ Replicated={false} // donors are always in the not replicated state since they are leftovers
139
+ DonorMode
140
+ />
141
+ <VDisk
142
+ VDiskId={{Domain: 2}}
143
+ VDiskState="Initial" // severity 3, yellow
144
+ Replicated={false}
145
+ DonorMode
146
+ />
147
+ <VDisk
148
+ VDiskId={{Domain: 3}}
149
+ VDiskState="PDiskError" // severity 5, red
150
+ Replicated={false}
151
+ DonorMode
152
+ />
153
+ </>
154
+ );
155
+
156
+ const [disk1, disk2, disk3] = getAllByRole('meter');
157
+
158
+ expect(disk1.className).not.toMatch(/_blue\b/i);
159
+ expect(disk1.className).toMatch(/_green\b/i);
160
+ expect(disk2.className).toMatch(/_yellow\b/i);
161
+ expect(disk3.className).toMatch(/_red\b/i);
162
+ });
163
+ });
@@ -1 +1,50 @@
1
+ import type {TVDiskStateInfo, TVSlotId} from '../../../types/api/storage';
2
+ import type {IStoragePoolGroup} from '../../../types/store/storage';
3
+
1
4
  export * from './constants';
5
+
6
+ export const isFullDonorData = (donor: TVDiskStateInfo | TVSlotId): donor is TVDiskStateInfo =>
7
+ 'VDiskId' in donor;
8
+
9
+ const generateEvaluator = (warn: number, crit: number) =>
10
+ (value: number) => {
11
+ if (0 <= value && value < warn) {
12
+ return 'success';
13
+ }
14
+
15
+ if (warn <= value && value < crit) {
16
+ return 'warning';
17
+ }
18
+
19
+ if (crit <= value) {
20
+ return 'danger';
21
+ }
22
+
23
+ return undefined;
24
+ };
25
+
26
+ const defaultDegradationEvaluator = generateEvaluator(1, 2);
27
+
28
+ const degradationEvaluators = {
29
+ 'block-4-2': generateEvaluator(1, 2),
30
+ 'mirror-3-dc': generateEvaluator(1, 3),
31
+ };
32
+
33
+ const canEvaluateErasureSpecies = (value?: string): value is keyof typeof degradationEvaluators =>
34
+ value !== undefined && value in degradationEvaluators;
35
+
36
+ export const getDegradedSeverity = (group: IStoragePoolGroup) => {
37
+ const evaluate = canEvaluateErasureSpecies(group.ErasureSpecies) ?
38
+ degradationEvaluators[group.ErasureSpecies] :
39
+ defaultDegradationEvaluator;
40
+
41
+ return evaluate(group.Missing);
42
+ };
43
+
44
+ export const getUsageSeverity = generateEvaluator(80, 85);
45
+
46
+ export const getUsage = (data: IStoragePoolGroup, step = 1) => {
47
+ const usage = Math.round((data.Used * 100) / data.Limit);
48
+
49
+ return Math.floor(usage / step) * step;
50
+ };
@@ -4,6 +4,15 @@ interface Window {
4
4
  params: {path: string},
5
5
  axiosOptions?: {concurrentId?: string},
6
6
  ) => Promise<import('../types/api/schema').TEvDescribeSchemeResult>;
7
+ getStorageInfo: (
8
+ params: {
9
+ tenant: string,
10
+ filter: string,
11
+ nodeId: string,
12
+ type: 'Groups' | 'Nodes',
13
+ },
14
+ axiosOptions?: {concurrentId?: string},
15
+ ) => Promise<import('../types/api/storage').TStorageInfo>;
7
16
  [method: string]: Function;
8
17
  };
9
18
  }