ydb-embedded-ui 1.10.3 → 1.12.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 (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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.12.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.11.1...v1.12.0) (2022-08-26)
4
+
5
+
6
+ ### Features
7
+
8
+ * **Storage:** show usage column ([73aed5f](https://github.com/ydb-platform/ydb-embedded-ui/commit/73aed5f9ed60b6d2bd77fd315ae514ee7443c489))
9
+ * **Storage:** vividly show degraded disks count ([7315a9c](https://github.com/ydb-platform/ydb-embedded-ui/commit/7315a9cfd98002a7fab85d721712aa82c6dbb552))
10
+
11
+ ## [1.11.1](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.11.0...v1.11.1) (2022-08-26)
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * number type instead of string for uint32 ([e60799e](https://github.com/ydb-platform/ydb-embedded-ui/commit/e60799edec4ef831e8c0d51f4384cde83520541d))
17
+ * **Storage:** expect arbitrary donors data ([09f8e08](https://github.com/ydb-platform/ydb-embedded-ui/commit/09f8e085c94faacd9da502643355e932346502ac))
18
+ * vdisk data contains pdisk data, not id ([bd1ea7f](https://github.com/ydb-platform/ydb-embedded-ui/commit/bd1ea7f59e0461256bb12f146b50470d21ac1ace))
19
+
20
+ ## [1.11.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.3...v1.11.0) (2022-08-23)
21
+
22
+
23
+ ### Features
24
+
25
+ * **Stack:** new component for stacked elements ([c42ba37](https://github.com/ydb-platform/ydb-embedded-ui/commit/c42ba37fafdd9dedc4be9d625d7e756a83c01fe3))
26
+ * **Storage:** display donor disks ([b808fe9](https://github.com/ydb-platform/ydb-embedded-ui/commit/b808fe951987c615f797af56017f8045a1ed852f))
27
+ * **VDisk:** display label for donors ([bba5ae8](https://github.com/ydb-platform/ydb-embedded-ui/commit/bba5ae8e44347a5b1d9cb72424f5a963a6848e59))
28
+
29
+
30
+ ### Bug Fixes
31
+
32
+ * **InfoViewer:** add size_s ([fc06451](https://github.com/ydb-platform/ydb-embedded-ui/commit/fc0645118f64a79f660d734c2ff43c42c738fd40))
33
+ * **PDisk:** new popup design ([9c0355d](https://github.com/ydb-platform/ydb-embedded-ui/commit/9c0355d4d9ccf69d43a5287b0e78d7c7993c4a18))
34
+ * **PDisk:** restrict component interface ([328efa9](https://github.com/ydb-platform/ydb-embedded-ui/commit/328efa90d214eca1bceeeb5bd9099aab36a3ddb0))
35
+ * **Storage:** shrink tooltip active area on Pool Name ([30a2b92](https://github.com/ydb-platform/ydb-embedded-ui/commit/30a2b92ff598d9caeabe17a4b8de214943945a91))
36
+ * **VDisk:** add a missing prop type ([39b6cf3](https://github.com/ydb-platform/ydb-embedded-ui/commit/39b6cf38811cab6c4374c77d3eb63c11fa7b83d5))
37
+ * **VDisk:** don't paint donors blue ([6b148b9](https://github.com/ydb-platform/ydb-embedded-ui/commit/6b148b914663a74e528a01a35f575f87ed6e9f09))
38
+ * **VDisk:** new popup design ([107b139](https://github.com/ydb-platform/ydb-embedded-ui/commit/107b13900b08631ea42034a6a2f7961c49c86556))
39
+
3
40
  ## [1.10.3](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.2...v1.10.3) (2022-08-23)
4
41
 
5
42
 
@@ -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,25 +1,53 @@
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
  }
45
+ &__usage-label {
46
+ &_overload {
47
+ color: var(--yc-color-text-light-primary);
48
+ background-color: var(--yc-color-base-danger-heavy);
49
+ }
50
+ }
23
51
  &__group-id {
24
52
  font-weight: 500;
25
53
  }