ydb-embedded-ui 1.10.2 → 1.11.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 (28) hide show
  1. package/CHANGELOG.md +36 -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 +25 -3
  15. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +39 -9
  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 +5 -0
  20. package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.js +3 -3
  21. package/dist/setupTests.js +8 -0
  22. package/dist/types/api/schema.ts +6 -14
  23. package/dist/types/api/storage.ts +118 -0
  24. package/dist/types/index.ts +1 -0
  25. package/dist/utils/index.js +22 -2
  26. package/package.json +28 -5
  27. package/dist/components/InfoViewer/InfoViewer.js +0 -47
  28. package/dist/index.test.js +0 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.11.1](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.11.0...v1.11.1) (2022-08-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * number type instead of string for uint32 ([e60799e](https://github.com/ydb-platform/ydb-embedded-ui/commit/e60799edec4ef831e8c0d51f4384cde83520541d))
9
+ * **Storage:** expect arbitrary donors data ([09f8e08](https://github.com/ydb-platform/ydb-embedded-ui/commit/09f8e085c94faacd9da502643355e932346502ac))
10
+ * vdisk data contains pdisk data, not id ([bd1ea7f](https://github.com/ydb-platform/ydb-embedded-ui/commit/bd1ea7f59e0461256bb12f146b50470d21ac1ace))
11
+
12
+ ## [1.11.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.3...v1.11.0) (2022-08-23)
13
+
14
+
15
+ ### Features
16
+
17
+ * **Stack:** new component for stacked elements ([c42ba37](https://github.com/ydb-platform/ydb-embedded-ui/commit/c42ba37fafdd9dedc4be9d625d7e756a83c01fe3))
18
+ * **Storage:** display donor disks ([b808fe9](https://github.com/ydb-platform/ydb-embedded-ui/commit/b808fe951987c615f797af56017f8045a1ed852f))
19
+ * **VDisk:** display label for donors ([bba5ae8](https://github.com/ydb-platform/ydb-embedded-ui/commit/bba5ae8e44347a5b1d9cb72424f5a963a6848e59))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * **InfoViewer:** add size_s ([fc06451](https://github.com/ydb-platform/ydb-embedded-ui/commit/fc0645118f64a79f660d734c2ff43c42c738fd40))
25
+ * **PDisk:** new popup design ([9c0355d](https://github.com/ydb-platform/ydb-embedded-ui/commit/9c0355d4d9ccf69d43a5287b0e78d7c7993c4a18))
26
+ * **PDisk:** restrict component interface ([328efa9](https://github.com/ydb-platform/ydb-embedded-ui/commit/328efa90d214eca1bceeeb5bd9099aab36a3ddb0))
27
+ * **Storage:** shrink tooltip active area on Pool Name ([30a2b92](https://github.com/ydb-platform/ydb-embedded-ui/commit/30a2b92ff598d9caeabe17a4b8de214943945a91))
28
+ * **VDisk:** add a missing prop type ([39b6cf3](https://github.com/ydb-platform/ydb-embedded-ui/commit/39b6cf38811cab6c4374c77d3eb63c11fa7b83d5))
29
+ * **VDisk:** don't paint donors blue ([6b148b9](https://github.com/ydb-platform/ydb-embedded-ui/commit/6b148b914663a74e528a01a35f575f87ed6e9f09))
30
+ * **VDisk:** new popup design ([107b139](https://github.com/ydb-platform/ydb-embedded-ui/commit/107b13900b08631ea42034a6a2f7961c49c86556))
31
+
32
+ ## [1.10.3](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.2...v1.10.3) (2022-08-23)
33
+
34
+
35
+ ### Bug Fixes
36
+
37
+ * **Overview:** format undefined values to empty string, not 0 ([1a37c27](https://github.com/ydb-platform/ydb-embedded-ui/commit/1a37c278328ad8eb4397d9507566829f01a9c872))
38
+
3
39
  ## [1.10.2](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.1...v1.10.2) (2022-08-17)
4
40
 
5
41
 
@@ -1,5 +1,5 @@
1
1
  import type {TEvDescribeSchemeResult, TIndexDescription} from '../../types/api/schema';
2
- import {InfoViewer, createInfoFormatter} from '../InfoViewer';
2
+ import {InfoViewer, createInfoFormatter, InfoViewerItem} from '../InfoViewer';
3
3
 
4
4
  const DISPLAYED_FIELDS: Set<keyof TIndexDescription> = new Set([
5
5
  'Type',
@@ -34,7 +34,7 @@ export const IndexInfoViewer = ({data}: IndexInfoViewerProps) => {
34
34
  }
35
35
 
36
36
  const TableIndex = data.PathDescription?.TableIndex;
37
- const info: Array<{label?: string, value?: unknown}> = [];
37
+ const info: Array<InfoViewerItem> = [];
38
38
 
39
39
  let key: keyof TIndexDescription;
40
40
  for (key in TableIndex) {
@@ -1,18 +1,23 @@
1
1
  .info-viewer {
2
- font-size: var(--yc-text-body-2-font-size);
3
- line-height: var(--yc-text-body-2-line-height);
2
+ --ydb-info-viewer-font-size: var(--yc-text-body-2-font-size);
3
+ --ydb-info-viewer-line-height: var(--yc-text-body-2-line-height);
4
+ --ydb-info-viewer-title-font-weight: 600;
5
+ --ydb-info-viewer-title-margin: 15px 0 10px;
6
+ --ydb-info-viewer-items-gap: 7px;
7
+
8
+ font-size: var(--ydb-info-viewer-font-size);
9
+ line-height: var(--ydb-info-viewer-line-height);
10
+
4
11
  &__title {
5
- margin: 15px 0 10px;
12
+ margin: var(--ydb-info-viewer-title-margin);
6
13
 
7
- font-size: var(--yc-text-body-2-font-size);
8
- font-weight: 600;
9
- line-height: var(--yc-text-body-2-line-height);
14
+ font-weight: var(--ydb-info-viewer-title-font-weight);
10
15
  }
11
16
 
12
17
  &__items {
13
18
  display: flex;
14
19
  flex-direction: column;
15
- gap: 7px;
20
+ gap: var(--ydb-info-viewer-items-gap);
16
21
 
17
22
  max-width: 100%;
18
23
  }
@@ -51,4 +56,24 @@
51
56
 
52
57
  white-space: nowrap;
53
58
  }
59
+
60
+ &_size {
61
+ &_s {
62
+ --ydb-info-viewer-font-size: var(--yc-text-body-1-font-size);
63
+ --ydb-info-viewer-line-height: var(--yc-text-body-1-line-height);
64
+ --ydb-info-viewer-title-font-weight: 500;
65
+ --ydb-info-viewer-title-margin: 0 0 4px;
66
+ --ydb-info-viewer-items-gap: 4px;
67
+
68
+ .info-viewer {
69
+ &__row {
70
+ height: auto;
71
+ }
72
+
73
+ &__label {
74
+ min-width: 85px;
75
+ }
76
+ }
77
+ }
78
+ }
54
79
  }
@@ -0,0 +1,43 @@
1
+ import type {ReactNode} from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ import './InfoViewer.scss';
5
+
6
+ export interface InfoViewerItem {
7
+ label: string;
8
+ value: ReactNode;
9
+ }
10
+
11
+ interface InfoViewerProps {
12
+ title?: string;
13
+ info?: InfoViewerItem[];
14
+ dots?: boolean;
15
+ size?: 's';
16
+ className?: string;
17
+ }
18
+
19
+ const b = cn('info-viewer');
20
+
21
+ const InfoViewer = ({title, info, dots = true, size, className}: InfoViewerProps) => (
22
+ <div className={b({size}, className)}>
23
+ {title && <div className={b('title')}>{title}</div>}
24
+ {info && info.length > 0 ? (
25
+ <div className={b('items')}>
26
+ {info.map((data, infoIndex) => (
27
+ <div className={b('row')} key={data.label + infoIndex}>
28
+ <div className={b('label')}>
29
+ {data.label}
30
+ {dots && <div className={b('dots')} />}
31
+ </div>
32
+
33
+ <div className={b('value')}>{data.value}</div>
34
+ </div>
35
+ ))}
36
+ </div>
37
+ ) : (
38
+ <>no {title} data</>
39
+ )}
40
+ </div>
41
+ );
42
+
43
+ export default InfoViewer;
@@ -2,3 +2,4 @@ import InfoViewer from './InfoViewer';
2
2
 
3
3
  export {InfoViewer};
4
4
  export * from './utils';
5
+ export type {InfoViewerItem} from './InfoViewer';
@@ -1,9 +1,11 @@
1
+ import type {ReactNode} from "react";
2
+
1
3
  type LabelMap<T> = {
2
4
  [label in keyof T]?: string;
3
5
  }
4
6
 
5
7
  type ValueFormatters<T> = {
6
- [label in keyof T]?: (value: T[label]) => string | undefined;
8
+ [label in keyof T]?: (value: T[label]) => ReactNode;
7
9
  }
8
10
 
9
11
  function formatLabel<Shape>(label: keyof Shape, map: LabelMap<Shape>) {
@@ -14,18 +16,18 @@ function formatValue<Shape, Key extends keyof Shape>(
14
16
  label: Key,
15
17
  value: Shape[Key],
16
18
  formatters: ValueFormatters<Shape>,
17
- defaultFormatter?: (value: Shape[Key]) => string | undefined,
19
+ defaultFormatter?: (value: Shape[Key]) => ReactNode,
18
20
  ) {
19
21
  const formatter = formatters[label] || defaultFormatter;
20
22
  const formattedValue = formatter ? formatter(value) : value;
21
23
 
22
- return String(formattedValue ?? '');
24
+ return formattedValue;
23
25
  }
24
26
 
25
27
  interface CreateInfoFormatterOptions<Shape> {
26
28
  values?: ValueFormatters<Shape>,
27
29
  labels?: LabelMap<Shape>,
28
- defaultValueFormatter?: (value: Shape[keyof Shape]) => string | undefined,
30
+ defaultValueFormatter?: (value: Shape[keyof Shape]) => ReactNode,
29
31
  }
30
32
 
31
33
  export function createInfoFormatter<Shape extends Record<string, any>>({
@@ -0,0 +1,55 @@
1
+ .stack {
2
+ --ydb-stack-base-z-index: 100;
3
+ --ydb-stack-offset-x: 4px;
4
+ --ydb-stack-offset-y: 4px;
5
+ --ydb-stack-offset-x-hover: 4px;
6
+ --ydb-stack-offset-y-hover: 8px;
7
+
8
+ position: relative;
9
+
10
+ &__layer {
11
+ transition: transform 0.1s ease-out;
12
+
13
+ &:first-child {
14
+ position: relative;
15
+ z-index: var(--ydb-stack-base-z-index);
16
+ }
17
+
18
+ & + & {
19
+ position: absolute;
20
+ z-index: calc(var(--ydb-stack-base-z-index) - var(--ydb-stack-level));
21
+ top: 0;
22
+ left: 0;
23
+
24
+ width: 100%;
25
+ height: 100%;
26
+
27
+ transform: translate(
28
+ calc(var(--ydb-stack-level) * var(--ydb-stack-offset-x)),
29
+ calc(var(--ydb-stack-level) * var(--ydb-stack-offset-y))
30
+ );
31
+ }
32
+ }
33
+
34
+ &:hover {
35
+ .stack__layer:first-child {
36
+ transform: translate(
37
+ calc(-1 * var(--ydb-stack-offset-x-hover)),
38
+ calc(-1 * var(--ydb-stack-offset-y-hover))
39
+ );
40
+ }
41
+
42
+ .stack__layer + .stack__layer {
43
+ transform: translate(
44
+ calc(
45
+ var(--ydb-stack-level) * (var(--ydb-stack-offset-x-hover) * 2) -
46
+ var(--ydb-stack-offset-x-hover)
47
+ ),
48
+ calc(
49
+ var(--ydb-stack-level) * (var(--ydb-stack-offset-y-hover) * 2) -
50
+ var(--ydb-stack-offset-y-hover)
51
+ )
52
+ );
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ import './Stack.scss';
5
+
6
+ interface StackProps {
7
+ className?: string;
8
+ }
9
+
10
+ const LAYER_CSS_VAR = '--ydb-stack-level';
11
+
12
+ const b = cn('stack');
13
+
14
+ export const Stack: React.FC<StackProps> = ({children, className}) => (
15
+ <div className={b(null, className)}>
16
+ {
17
+ React.Children.map(children, (child, index) => {
18
+ if (!React.isValidElement(child)) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <div
24
+ className={b('layer')}
25
+ style={{
26
+ [LAYER_CSS_VAR]: index,
27
+ } as React.CSSProperties}
28
+ >
29
+ {child}
30
+ </div>
31
+ );
32
+ })
33
+ }
34
+ </div>
35
+ );
@@ -7,6 +7,8 @@
7
7
  width: 100%;
8
8
  height: var(--yc-text-body-2-line-height);
9
9
 
10
+ vertical-align: top;
11
+
10
12
  border: 1px solid var(--yc-color-infographics-misc-medium);
11
13
  border-radius: 2px;
12
14
  background-color: var(--yc-color-infographics-misc-light);
@@ -46,6 +46,11 @@ function DiskStateProgressBar({
46
46
  ? b({[diskProgressColors[severity].toLowerCase()]: true})
47
47
  : undefined
48
48
  }
49
+ role="meter"
50
+ aria-label="Disk allocated space"
51
+ aria-valuemin={0}
52
+ aria-valuemax={100}
53
+ aria-valuenow={diskAllocatedPercent}
49
54
  >
50
55
  {href ? (
51
56
  <InternalLink to={href} className={b('link')}>
@@ -11,25 +11,8 @@
11
11
  &:last-child {
12
12
  margin-right: 0px;
13
13
  }
14
- &__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;
24
-
25
- margin: 5px 0;
26
14
 
27
- font-weight: 500;
28
- text-align: center;
29
-
30
- border-bottom: 1px solid var(--yc-color-line-generic);
31
- }
32
- &__property {
33
- text-align: right;
15
+ &__popup-wrapper {
16
+ padding: 12px;
34
17
  }
35
18
  }
@@ -1,7 +1,8 @@
1
1
  import React, {useEffect, useState, useRef, useMemo} from 'react';
2
2
  import cn from 'bem-cn-lite';
3
- import _ from 'lodash';
4
3
  import {Popup} from '@yandex-cloud/uikit';
4
+
5
+ import type {RequiredField} from '../../../types';
5
6
  //@ts-ignore
6
7
  import {bytesToGB} from '../../../utils/utils';
7
8
  //@ts-ignore
@@ -10,6 +11,7 @@ import routes, {createHref} from '../../../routes';
10
11
  import {getPDiskId} from '../../../utils';
11
12
  import {getPDiskType} from '../../../utils/pdisk';
12
13
  import {TPDiskStateInfo, TPDiskState} from '../../../types/api/storage';
14
+ import {InfoViewer} from '../../../components/InfoViewer';
13
15
  import DiskStateProgressBar, {
14
16
  diskProgressColors,
15
17
  } from '../DiskStateProgressBar/DiskStateProgressBar';
@@ -38,7 +40,7 @@ const stateSeverity = {
38
40
  [TPDiskState.DeviceIoError]: 5,
39
41
  };
40
42
 
41
- type PDiskProps = TPDiskStateInfo;
43
+ type PDiskProps = RequiredField<TPDiskStateInfo, 'NodeId'>;
42
44
 
43
45
  const isSeverityKey = (key?: TPDiskState): key is keyof typeof stateSeverity =>
44
46
  key !== undefined && key in stateSeverity;
@@ -76,51 +78,46 @@ function Pdisk(props: PDiskProps) {
76
78
  diskProgressColors[colorSeverity.Yellow as keyof typeof diskProgressColors],
77
79
  ];
78
80
 
79
- const pdiskData: {property: string; value: string | number}[] = [
80
- {property: 'PDisk', value: getPDiskId({NodeId, PDiskId})},
81
+ const pdiskData: {label: string; value: string | number}[] = [
82
+ {label: 'PDisk', value: getPDiskId({NodeId, PDiskId})},
81
83
  ];
82
84
 
83
- pdiskData.push({property: 'State', value: State || 'not available'});
84
- pdiskData.push({property: 'Type', value: getPDiskType(props) || 'unknown'});
85
- NodeId && pdiskData.push({property: 'Node Id', value: NodeId});
85
+ pdiskData.push({label: 'State', value: State || 'not available'});
86
+ pdiskData.push({label: 'Type', value: getPDiskType(props) || 'unknown'});
87
+ NodeId && pdiskData.push({label: 'Node Id', value: NodeId});
86
88
 
87
- Path && pdiskData.push({property: 'Path', value: Path});
89
+ Path && pdiskData.push({label: 'Path', value: Path});
88
90
  pdiskData.push({
89
- property: 'Available',
91
+ label: 'Available',
90
92
  value: `${bytesToGB(AvailableSize)} of ${bytesToGB(TotalSize)}`,
91
93
  });
92
94
  Realtime &&
93
95
  errorColors.includes(Realtime) &&
94
- pdiskData.push({property: 'Realtime', value: Realtime});
96
+ pdiskData.push({label: 'Realtime', value: Realtime});
95
97
  Device &&
96
98
  errorColors.includes(Device) &&
97
- pdiskData.push({property: 'Device', value: Device});
99
+ pdiskData.push({label: 'Device', value: Device});
98
100
  return pdiskData;
99
101
  };
100
102
  /* eslint-enable */
101
103
 
102
- const renderPopup = () => {
103
- const pdiskData = preparePdiskData();
104
- return (
105
- <Popup
106
- className={b('popup-wrapper')}
107
- anchorRef={anchor}
108
- open={isPopupVisible}
109
- placement={['top', 'bottom']}
110
- hasArrow
111
- >
112
- <div className={b('popup-content')}>
113
- <div className={b('popup-section-name')}>PDisk</div>
114
- {_.map(pdiskData, (row) => (
115
- <React.Fragment key={row.property}>
116
- <div className={b('property')}>{row.property}</div>
117
- <div className={b('value')}>{row.value}</div>
118
- </React.Fragment>
119
- ))}
120
- </div>
121
- </Popup>
122
- );
123
- };
104
+ const renderPopup = () => (
105
+ <Popup
106
+ className={b('popup-wrapper')}
107
+ anchorRef={anchor}
108
+ open={isPopupVisible}
109
+ placement={['top', 'bottom']}
110
+ // bigger offset for easier switching to neighbour nodes
111
+ // matches the default offset for popup with arrow out of a sense of beauty
112
+ offset={[0, 12]}
113
+ >
114
+ <InfoViewer
115
+ title="PDisk"
116
+ info={preparePdiskData()}
117
+ size="s"
118
+ />
119
+ </Popup>
120
+ );
124
121
 
125
122
  const pdiskAllocatedPercent = useMemo(() => {
126
123
  const {AvailableSize, TotalSize} = props;
@@ -0,0 +1,40 @@
1
+ import {render} from '@testing-library/react'
2
+ import {MemoryRouter} from 'react-router-dom';
3
+
4
+ import {TPDiskState} from '../../../../types/api/storage'
5
+
6
+ import PDisk from '../Pdisk'
7
+
8
+ describe('PDisk state', () => {
9
+ it('Should determine severity based on State', () => {
10
+ const {getAllByRole} = render(
11
+ <MemoryRouter>
12
+ <PDisk
13
+ NodeId={1}
14
+ State={TPDiskState.Normal}
15
+ />
16
+ <PDisk
17
+ NodeId={2}
18
+ State={TPDiskState.ChunkQuotaError}
19
+ />
20
+ </MemoryRouter>
21
+ );
22
+
23
+ const [normalDisk, erroredDisk] = getAllByRole('meter');
24
+
25
+ expect(normalDisk.className).not.toBe(erroredDisk.className);
26
+ });
27
+
28
+ it('Should display as unavailabe when no State is provided', () => {
29
+ const {getByRole} = render(
30
+ <MemoryRouter>
31
+ <PDisk NodeId={1} />
32
+ </MemoryRouter>
33
+ );
34
+
35
+ const disk = getByRole('meter');
36
+
37
+ // unavailable disks display with the highest severity
38
+ expect(disk.className).toMatch(/_red\b/i);
39
+ });
40
+ });
@@ -1,23 +1,45 @@
1
1
  .global-storage-groups {
2
+ &__vdisks-column {
3
+ overflow: visible; // to enable stacked disks overflow the row
4
+ }
5
+
2
6
  &__vdisks-wrapper {
3
7
  display: flex;
4
- overflow-x: auto;
5
- overflow-y: hidden;
6
8
  justify-content: center;
7
9
 
8
10
  min-width: 500px;
9
11
  }
12
+ &__vdisks-item {
13
+ flex-grow: 1;
14
+
15
+ max-width: 200px;
16
+ margin-right: 10px;
17
+
18
+ &:last-child {
19
+ margin-right: 0px;
20
+ }
21
+
22
+ .stack__layer {
23
+ background: var(--yc-color-base-background);
24
+
25
+ .data-table__row:hover & {
26
+ background: var(--yc-color-base-float-hover);
27
+ }
28
+ }
29
+ }
10
30
  &__pool-name-wrapper {
11
31
  display: flex;
12
32
  align-items: flex-end;
33
+
34
+ width: 230px;
13
35
  }
14
36
  &__pool-name {
15
37
  display: inline-block;
16
38
  overflow: hidden;
17
39
 
18
- width: 230px;
19
40
  max-width: 230px;
20
41
 
42
+ vertical-align: top;
21
43
  text-overflow: ellipsis;
22
44
  }
23
45
  &__group-id {
@@ -3,9 +3,11 @@ import cn from 'bem-cn-lite';
3
3
  import DataTable, {Column, Settings, SortOrder} from '@yandex-cloud/react-data-table';
4
4
  import {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} from '../utils';
20
+
16
21
  import './StorageGroups.scss';
17
22
 
18
23
  enum TableColumnsIds {
@@ -190,17 +195,42 @@ function StorageGroups({data, tableSettings, visibleEntities, nodes}: StorageGro
190
195
  },
191
196
  {
192
197
  name: TableColumnsIds.VDisks,
198
+ className: b('vdisks-column'),
193
199
  header: tableColumnsNames[TableColumnsIds.VDisks],
194
200
  render: ({value, row}) => (
195
201
  <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
- ))}
202
+ {_.map(value as TVDiskStateInfo[], (el) => {
203
+ const donors = Array.isArray(el.Donors) ? el.Donors.filter(isFullDonorData) : [];
204
+
205
+ return (
206
+ donors.length > 0 ? (
207
+ <Stack className={b('vdisks-item')} key={stringifyVdiskId(el.VDiskId)}>
208
+ <Vdisk
209
+ {...el}
210
+ PoolName={row[TableColumnsIds.PoolName]}
211
+ nodes={nodes}
212
+ />
213
+ {donors.map((donor) => (
214
+ <Vdisk
215
+ {...donor}
216
+ // donor and acceptor are always in the same group
217
+ PoolName={row[TableColumnsIds.PoolName]}
218
+ nodes={nodes}
219
+ key={stringifyVdiskId(donor.VDiskId)}
220
+ />
221
+ ))}
222
+ </Stack>
223
+ ) : (
224
+ <div className={b('vdisks-item')} key={stringifyVdiskId(el.VDiskId)}>
225
+ <Vdisk
226
+ {...el}
227
+ PoolName={row[TableColumnsIds.PoolName]}
228
+ nodes={nodes}
229
+ />
230
+ </div>
231
+ )
232
+ );
233
+ })}
204
234
  </div>
205
235
  ),
206
236
  align: DataTable.CENTER,