ydb-embedded-ui 1.10.1 → 1.11.0

Sign up to get free protection for your applications and to get access to all the features.
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
  ),