ydb-embedded-ui 1.10.1 → 1.11.0

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 +41 -0
  2. package/dist/components/IndexInfoViewer/IndexInfoViewer.tsx +12 -9
  3. package/dist/components/InfoViewer/InfoViewer.scss +33 -9
  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 +21 -11
  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 +31 -7
  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/Tenant/Diagnostics/DetailedOverview/DetailedOverview.tsx +15 -14
  20. package/dist/containers/Tenant/QueryEditor/QueryEditor.js +12 -2
  21. package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.js +164 -42
  22. package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.scss +18 -0
  23. package/dist/services/api.js +0 -1
  24. package/dist/setupTests.js +8 -0
  25. package/dist/store/reducers/executeQuery.js +3 -2
  26. package/dist/store/reducers/settings.js +20 -13
  27. package/dist/types/api/schema.ts +117 -4
  28. package/dist/types/api/storage.ts +121 -0
  29. package/dist/types/index.ts +1 -0
  30. package/dist/utils/constants.js +4 -0
  31. package/dist/utils/index.js +28 -4
  32. package/dist/utils/pdisk.ts +2 -2
  33. package/package.json +28 -5
  34. package/dist/components/InfoViewer/InfoViewer.js +0 -47
  35. package/dist/index.test.js +0 -5
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.11.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.3...v1.11.0) (2022-08-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * **Stack:** new component for stacked elements ([c42ba37](https://github.com/ydb-platform/ydb-embedded-ui/commit/c42ba37fafdd9dedc4be9d625d7e756a83c01fe3))
9
+ * **Storage:** display donor disks ([b808fe9](https://github.com/ydb-platform/ydb-embedded-ui/commit/b808fe951987c615f797af56017f8045a1ed852f))
10
+ * **VDisk:** display label for donors ([bba5ae8](https://github.com/ydb-platform/ydb-embedded-ui/commit/bba5ae8e44347a5b1d9cb72424f5a963a6848e59))
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **InfoViewer:** add size_s ([fc06451](https://github.com/ydb-platform/ydb-embedded-ui/commit/fc0645118f64a79f660d734c2ff43c42c738fd40))
16
+ * **PDisk:** new popup design ([9c0355d](https://github.com/ydb-platform/ydb-embedded-ui/commit/9c0355d4d9ccf69d43a5287b0e78d7c7993c4a18))
17
+ * **PDisk:** restrict component interface ([328efa9](https://github.com/ydb-platform/ydb-embedded-ui/commit/328efa90d214eca1bceeeb5bd9099aab36a3ddb0))
18
+ * **Storage:** shrink tooltip active area on Pool Name ([30a2b92](https://github.com/ydb-platform/ydb-embedded-ui/commit/30a2b92ff598d9caeabe17a4b8de214943945a91))
19
+ * **VDisk:** add a missing prop type ([39b6cf3](https://github.com/ydb-platform/ydb-embedded-ui/commit/39b6cf38811cab6c4374c77d3eb63c11fa7b83d5))
20
+ * **VDisk:** don't paint donors blue ([6b148b9](https://github.com/ydb-platform/ydb-embedded-ui/commit/6b148b914663a74e528a01a35f575f87ed6e9f09))
21
+ * **VDisk:** new popup design ([107b139](https://github.com/ydb-platform/ydb-embedded-ui/commit/107b13900b08631ea42034a6a2f7961c49c86556))
22
+
23
+ ## [1.10.3](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.2...v1.10.3) (2022-08-23)
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * **Overview:** format undefined values to empty string, not 0 ([1a37c27](https://github.com/ydb-platform/ydb-embedded-ui/commit/1a37c278328ad8eb4397d9507566829f01a9c872))
29
+
30
+ ## [1.10.2](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.1...v1.10.2) (2022-08-17)
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * convert bytes on decimal scale ([db9b0a7](https://github.com/ydb-platform/ydb-embedded-ui/commit/db9b0a71fc5334f5a40992cc6abc0688782ad5d2))
36
+ * display HDD instead of ROT as pdisk type ([bd9e5ba](https://github.com/ydb-platform/ydb-embedded-ui/commit/bd9e5ba4e594cb3a1f6a964f619f9824e083ae7c))
37
+ * **InfoViewer:** accept default value formatter ([e03d8cc](https://github.com/ydb-platform/ydb-embedded-ui/commit/e03d8cc5de76e4ac00b05586ae6f6522a9708fb0))
38
+ * **InfoViewer:** allow longer labels ([89060a3](https://github.com/ydb-platform/ydb-embedded-ui/commit/89060a381858b5beaa3c3cf3402c13c917705676))
39
+ * **Overview:** display table r/o replicas ([6dbe0b4](https://github.com/ydb-platform/ydb-embedded-ui/commit/6dbe0b45fc5e3867f9d6141d270c15508a693e35))
40
+ * **Overview:** format & group table info in overview ([1a35cfc](https://github.com/ydb-platform/ydb-embedded-ui/commit/1a35cfcd2075454c4a1f1fc4961a4b3106b6d225))
41
+ * **QueryEditor:** save chosen run action ([b0fb436](https://github.com/ydb-platform/ydb-embedded-ui/commit/b0fb43651e0c6d1dc5d6a25f92716703402b556d))
42
+ * use current i18n lang for numeral formatting ([5d58fcf](https://github.com/ydb-platform/ydb-embedded-ui/commit/5d58fcffde21924f3cbe6c28946c7a9f755a8490))
43
+
3
44
  ## [1.10.1](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.0...v1.10.1) (2022-08-10)
4
45
 
5
46
 
@@ -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',
@@ -10,13 +10,16 @@ const DISPLAYED_FIELDS: Set<keyof TIndexDescription> = new Set([
10
10
  ]);
11
11
 
12
12
  const formatItem = createInfoFormatter<TIndexDescription>({
13
- Type: (value) => value?.substring(10), // trims EIndexType prefix
14
- State: (value) => value?.substring(11), // trims EIndexState prefix
15
- KeyColumnNames: (value) => value?.join(', '),
16
- DataColumnNames: (value) => value?.join(', '),
17
- }, {
18
- KeyColumnNames: 'Columns',
19
- DataColumnNames: 'Includes',
13
+ values: {
14
+ Type: (value) => value?.substring(10), // trims EIndexType prefix
15
+ State: (value) => value?.substring(11), // trims EIndexState prefix
16
+ KeyColumnNames: (value) => value?.join(', '),
17
+ DataColumnNames: (value) => value?.join(', '),
18
+ },
19
+ labels: {
20
+ KeyColumnNames: 'Columns',
21
+ DataColumnNames: 'Includes',
22
+ },
20
23
  });
21
24
 
22
25
  interface IndexInfoViewerProps {
@@ -31,7 +34,7 @@ export const IndexInfoViewer = ({data}: IndexInfoViewerProps) => {
31
34
  }
32
35
 
33
36
  const TableIndex = data.PathDescription?.TableIndex;
34
- const info: Array<{label?: string, value?: unknown}> = [];
37
+ const info: Array<InfoViewerItem> = [];
35
38
 
36
39
  let key: keyof TIndexDescription;
37
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
  }
@@ -27,11 +32,10 @@
27
32
 
28
33
  &__label {
29
34
  display: flex;
30
- flex: 1 1 auto;
35
+ flex: 0 1 auto;
31
36
  align-items: baseline;
32
37
 
33
38
  min-width: 200px;
34
- max-width: 200px;
35
39
 
36
40
  white-space: nowrap;
37
41
 
@@ -52,4 +56,24 @@
52
56
 
53
57
  white-space: nowrap;
54
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
+ }
55
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
- type FieldMappers<T> = {
6
- [label in keyof T]?: (value: T[label]) => string | undefined;
7
+ type ValueFormatters<T> = {
8
+ [label in keyof T]?: (value: T[label]) => ReactNode;
7
9
  }
8
10
 
9
11
  function formatLabel<Shape>(label: keyof Shape, map: LabelMap<Shape>) {
@@ -13,20 +15,28 @@ function formatLabel<Shape>(label: keyof Shape, map: LabelMap<Shape>) {
13
15
  function formatValue<Shape, Key extends keyof Shape>(
14
16
  label: Key,
15
17
  value: Shape[Key],
16
- mappers: FieldMappers<Shape>,
18
+ formatters: ValueFormatters<Shape>,
19
+ defaultFormatter?: (value: Shape[Key]) => ReactNode,
17
20
  ) {
18
- const mapper = mappers[label];
19
- const mappedValue = mapper ? mapper(value) : value;
21
+ const formatter = formatters[label] || defaultFormatter;
22
+ const formattedValue = formatter ? formatter(value) : value;
20
23
 
21
- return String(mappedValue ?? '');
24
+ return formattedValue;
22
25
  }
23
26
 
24
- export function createInfoFormatter<Shape extends Record<string, any>>(
25
- fieldMappers?: FieldMappers<Shape>,
26
- labelMap?: LabelMap<Shape>,
27
- ) {
27
+ interface CreateInfoFormatterOptions<Shape> {
28
+ values?: ValueFormatters<Shape>,
29
+ labels?: LabelMap<Shape>,
30
+ defaultValueFormatter?: (value: Shape[keyof Shape]) => ReactNode,
31
+ }
32
+
33
+ export function createInfoFormatter<Shape extends Record<string, any>>({
34
+ values: valueFormatters,
35
+ labels: labelMap,
36
+ defaultValueFormatter,
37
+ }: CreateInfoFormatterOptions<Shape>) {
28
38
  return <Key extends keyof Shape>(label: Key, value: Shape[Key]) => ({
29
39
  label: formatLabel(label, labelMap || {}),
30
- value: formatValue(label, value, fieldMappers || {}),
40
+ value: formatValue(label, value, valueFormatters || {}, defaultValueFormatter),
31
41
  });
32
42
  }
@@ -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 {
@@ -4,8 +4,11 @@ import DataTable, {Column, Settings, SortOrder} from '@yandex-cloud/react-data-t
4
4
  import {Popover, PopoverBehavior} from '@yandex-cloud/uikit';
5
5
 
6
6
  import Vdisk from '../Vdisk/Vdisk';
7
+ import {Stack} from '../../../components/Stack/Stack';
7
8
  //@ts-ignore
8
9
  import EntityStatus from '../../../components/EntityStatus/EntityStatus';
10
+
11
+ import {TVDiskStateInfo} from '../../../types/api/storage';
9
12
  //@ts-ignore
10
13
  import {VisibleEntities} from '../../../store/reducers/storage';
11
14
  //@ts-ignore
@@ -190,16 +193,37 @@ function StorageGroups({data, tableSettings, visibleEntities, nodes}: StorageGro
190
193
  },
191
194
  {
192
195
  name: TableColumnsIds.VDisks,
196
+ className: b('vdisks-column'),
193
197
  header: tableColumnsNames[TableColumnsIds.VDisks],
194
198
  render: ({value, row}) => (
195
199
  <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
- />
200
+ {_.map(value as TVDiskStateInfo[], (el) => (
201
+ Array.isArray(el.Donors) && el.Donors.length > 0 ? (
202
+ <Stack className={b('vdisks-item')} key={stringifyVdiskId(el.VDiskId)}>
203
+ <Vdisk
204
+ {...el}
205
+ PoolName={row[TableColumnsIds.PoolName]}
206
+ nodes={nodes}
207
+ />
208
+ {el.Donors.map((donor) => (
209
+ <Vdisk
210
+ {...donor}
211
+ // donor and acceptor are always in the same group
212
+ PoolName={row[TableColumnsIds.PoolName]}
213
+ nodes={nodes}
214
+ key={stringifyVdiskId(donor.VDiskId)}
215
+ />
216
+ ))}
217
+ </Stack>
218
+ ) : (
219
+ <div className={b('vdisks-item')} key={stringifyVdiskId(el.VDiskId)}>
220
+ <Vdisk
221
+ {...el}
222
+ PoolName={row[TableColumnsIds.PoolName]}
223
+ nodes={nodes}
224
+ />
225
+ </div>
226
+ )
203
227
  ))}
204
228
  </div>
205
229
  ),