ydb-embedded-ui 1.10.3 → 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.
- package/CHANGELOG.md +20 -0
- package/dist/components/IndexInfoViewer/IndexInfoViewer.tsx +2 -2
- package/dist/components/InfoViewer/InfoViewer.scss +32 -7
- package/dist/components/InfoViewer/InfoViewer.tsx +43 -0
- package/dist/components/InfoViewer/index.ts +1 -0
- package/dist/components/InfoViewer/utils.ts +6 -4
- package/dist/components/Stack/Stack.scss +55 -0
- package/dist/components/Stack/Stack.tsx +35 -0
- package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.scss +2 -0
- package/dist/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +5 -0
- package/dist/containers/Storage/Pdisk/Pdisk.scss +2 -19
- package/dist/containers/Storage/Pdisk/Pdisk.tsx +30 -33
- package/dist/containers/Storage/Pdisk/__tests__/colors.tsx +40 -0
- package/dist/containers/Storage/StorageGroups/StorageGroups.scss +25 -3
- package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +31 -7
- package/dist/containers/Storage/Vdisk/Vdisk.js +63 -64
- package/dist/containers/Storage/Vdisk/Vdisk.scss +9 -28
- package/dist/containers/Storage/Vdisk/__tests__/colors.tsx +163 -0
- package/dist/setupTests.js +8 -0
- package/dist/types/api/storage.ts +121 -0
- package/dist/types/index.ts +1 -0
- package/package.json +28 -5
- package/dist/components/InfoViewer/InfoViewer.js +0 -47
- package/dist/index.test.js +0 -5
package/CHANGELOG.md
CHANGED
@@ -1,5 +1,25 @@
|
|
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
|
+
|
3
23
|
## [1.10.3](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.10.2...v1.10.3) (2022-08-23)
|
4
24
|
|
5
25
|
|
@@ -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<
|
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:
|
12
|
+
margin: var(--ydb-info-viewer-title-margin);
|
6
13
|
|
7
|
-
font-
|
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:
|
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;
|
@@ -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]) =>
|
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]) =>
|
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
|
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]) =>
|
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
|
+
);
|
@@ -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
|
-
|
28
|
-
|
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: {
|
80
|
-
{
|
81
|
+
const pdiskData: {label: string; value: string | number}[] = [
|
82
|
+
{label: 'PDisk', value: getPDiskId({NodeId, PDiskId})},
|
81
83
|
];
|
82
84
|
|
83
|
-
pdiskData.push({
|
84
|
-
pdiskData.push({
|
85
|
-
NodeId && pdiskData.push({
|
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({
|
89
|
+
Path && pdiskData.push({label: 'Path', value: Path});
|
88
90
|
pdiskData.push({
|
89
|
-
|
91
|
+
label: 'Available',
|
90
92
|
value: `${bytesToGB(AvailableSize)} of ${bytesToGB(TotalSize)}`,
|
91
93
|
});
|
92
94
|
Realtime &&
|
93
95
|
errorColors.includes(Realtime) &&
|
94
|
-
pdiskData.push({
|
96
|
+
pdiskData.push({label: 'Realtime', value: Realtime});
|
95
97
|
Device &&
|
96
98
|
errorColors.includes(Device) &&
|
97
|
-
pdiskData.push({
|
99
|
+
pdiskData.push({label: 'Device', value: Device});
|
98
100
|
return pdiskData;
|
99
101
|
};
|
100
102
|
/* eslint-enable */
|
101
103
|
|
102
|
-
const renderPopup = () =>
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
197
|
-
|
198
|
-
key={stringifyVdiskId(el.VDiskId)}
|
199
|
-
|
200
|
-
|
201
|
-
|
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
|
),
|
@@ -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
|
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
|
-
|
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 = [{
|
99
|
-
vdiskData.push({
|
100
|
-
PoolName && vdiskData.push({
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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({
|
138
|
+
vdiskData.push({label: 'Space', value: DiskSpace});
|
133
139
|
|
134
140
|
FrontQueues &&
|
135
141
|
FrontQueues !== diskProgressColors[colorSeverity.Green] &&
|
136
|
-
vdiskData.push({
|
142
|
+
vdiskData.push({label: 'FrontQueues', value: FrontQueues});
|
137
143
|
|
138
|
-
!Replicated && vdiskData.push({
|
144
|
+
!Replicated && vdiskData.push({label: 'Replicated', value: 'NO'});
|
139
145
|
|
140
|
-
UnsyncedVDisks && vdiskData.push({
|
146
|
+
UnsyncedVDisks && vdiskData.push({label: 'UnsyncVDisks', value: UnsyncedVDisks});
|
141
147
|
|
142
148
|
Boolean(Number(AllocatedSize)) &&
|
143
149
|
vdiskData.push({
|
144
|
-
|
150
|
+
label: 'Allocated',
|
145
151
|
value: bytesToGB(AllocatedSize),
|
146
152
|
});
|
147
153
|
|
148
154
|
Boolean(Number(ReadThroughput)) &&
|
149
|
-
vdiskData.push({
|
155
|
+
vdiskData.push({label: 'Read', value: bytesToSpeed(ReadThroughput)});
|
150
156
|
|
151
157
|
Boolean(Number(WriteThroughput)) &&
|
152
158
|
vdiskData.push({
|
153
|
-
|
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 = [{
|
174
|
+
const pdiskData = [{label: 'PDisk', value: getPDiskId(PDisk)}];
|
169
175
|
pdiskData.push({
|
170
|
-
|
176
|
+
label: 'State',
|
171
177
|
value: PDisk.State || 'not available',
|
172
178
|
});
|
173
|
-
pdiskData.push({
|
174
|
-
PDisk.NodeId && pdiskData.push({
|
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({
|
178
|
-
PDisk.Path && pdiskData.push({
|
183
|
+
pdiskData.push({label: 'Host', value: nodes[PDisk.NodeId]});
|
184
|
+
PDisk.Path && pdiskData.push({label: 'Path', value: PDisk.Path});
|
179
185
|
pdiskData.push({
|
180
|
-
|
186
|
+
label: 'Available',
|
181
187
|
value: `${bytesToGB(PDisk.AvailableSize)} of ${bytesToGB(PDisk.TotalSize)}`,
|
182
188
|
});
|
183
189
|
errorColors.includes(PDisk.Realtime) &&
|
184
|
-
pdiskData.push({
|
190
|
+
pdiskData.push({label: 'Realtime', value: PDisk.Realtime});
|
185
191
|
errorColors.includes(PDisk.Device) &&
|
186
|
-
pdiskData.push({
|
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
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
>
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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:
|
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
|
-
|
5
|
+
.info-viewer + .info-viewer {
|
6
|
+
margin-top: 8px;
|
7
|
+
padding-top: 8px;
|
26
8
|
|
27
|
-
|
28
|
-
|
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
|
-
|
33
|
-
|
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
|
+
});
|
package/dist/setupTests.js
CHANGED
@@ -3,3 +3,11 @@
|
|
3
3
|
// expect(element).toHaveTextContent(/react/i)
|
4
4
|
// learn more: https://github.com/testing-library/jest-dom
|
5
5
|
import '@testing-library/jest-dom';
|
6
|
+
|
7
|
+
import {configure as configureUiKit} from '@yandex-cloud/uikit';
|
8
|
+
import {configure as configureYdbUiComponents} from 'ydb-ui-components';
|
9
|
+
import {i18n, Lang} from '../src/utils/i18n';
|
10
|
+
|
11
|
+
i18n.setLang(Lang.En);
|
12
|
+
configureYdbUiComponents({lang: Lang.En});
|
13
|
+
configureUiKit({lang: Lang.En});
|
@@ -52,3 +52,124 @@ export interface TPDiskStateInfo {
|
|
52
52
|
Overall?: EFlag;
|
53
53
|
SerialNumber?: string;
|
54
54
|
}
|
55
|
+
|
56
|
+
export enum EVDiskState {
|
57
|
+
Initial = 'Initial',
|
58
|
+
LocalRecoveryError = 'LocalRecoveryError',
|
59
|
+
SyncGuidRecovery = 'SyncGuidRecovery',
|
60
|
+
SyncGuidRecoveryError = 'SyncGuidRecoveryError',
|
61
|
+
OK = 'OK',
|
62
|
+
PDiskError = 'PDiskError',
|
63
|
+
}
|
64
|
+
|
65
|
+
interface TRank {
|
66
|
+
/**
|
67
|
+
* uint32
|
68
|
+
* Rank in percents; 0-100% is good; >100% is bad.
|
69
|
+
* Formula for rank calculation is the following:
|
70
|
+
* Rank = actual_value / max_allowed_value * 100
|
71
|
+
*/
|
72
|
+
RankPercent?: string;
|
73
|
+
|
74
|
+
/**
|
75
|
+
* Flag is the Rank transformed to something simple
|
76
|
+
* to understand: Green, Yellow or Red
|
77
|
+
*/
|
78
|
+
Flag?: EFlag;
|
79
|
+
}
|
80
|
+
|
81
|
+
interface TVDiskSatisfactionRank {
|
82
|
+
FreshRank?: TRank;
|
83
|
+
LevelRank?: TRank;
|
84
|
+
}
|
85
|
+
|
86
|
+
interface TVDiskID {
|
87
|
+
/** uint32 */
|
88
|
+
GroupID?: string;
|
89
|
+
/** uint32 */
|
90
|
+
GroupGeneration?: string;
|
91
|
+
/** uint32 */
|
92
|
+
Ring?: string;
|
93
|
+
/** uint32 */
|
94
|
+
Domain?: string;
|
95
|
+
/** uint32 */
|
96
|
+
VDisk?: string;
|
97
|
+
}
|
98
|
+
|
99
|
+
export interface TVDiskStateInfo {
|
100
|
+
VDiskId?: TVDiskID;
|
101
|
+
/** uint64 */
|
102
|
+
CreateTime?: string;
|
103
|
+
/** uint64 */
|
104
|
+
ChangeTime?: string;
|
105
|
+
/** uint32 */
|
106
|
+
PDiskId?: string;
|
107
|
+
/** uint32 */
|
108
|
+
VDiskSlotId?: string;
|
109
|
+
/** uint64 */
|
110
|
+
Guid?: string;
|
111
|
+
/** uint64 */
|
112
|
+
Kind?: string;
|
113
|
+
/** uint32 */
|
114
|
+
NodeId?: string;
|
115
|
+
/** uint32 */
|
116
|
+
Count?: string;
|
117
|
+
|
118
|
+
Overall?: EFlag;
|
119
|
+
|
120
|
+
/** Current state of VDisk */
|
121
|
+
VDiskState?: EVDiskState;
|
122
|
+
/** Disk space flags */
|
123
|
+
DiskSpace?: EFlag;
|
124
|
+
/** Compaction satisfaction rank */
|
125
|
+
SatisfactionRank?: TVDiskSatisfactionRank;
|
126
|
+
/** Is VDisk replicated? (i.e. contains all blobs it must have) */
|
127
|
+
Replicated?: boolean;
|
128
|
+
/** Does this VDisk has any yet unreplicated phantom-like blobs? */
|
129
|
+
UnreplicatedPhantoms?: boolean;
|
130
|
+
/** The same for the non-phantom-like blobs. */
|
131
|
+
UnreplicatedNonPhantoms?: boolean;
|
132
|
+
/**
|
133
|
+
* uint64
|
134
|
+
* How many unsynced VDisks from current BlobStorage group we see
|
135
|
+
*/
|
136
|
+
UnsyncedVDisks?: string;
|
137
|
+
/**
|
138
|
+
* uint64
|
139
|
+
* How much this VDisk have allocated on corresponding PDisk
|
140
|
+
*/
|
141
|
+
AllocatedSize?: string;
|
142
|
+
/**
|
143
|
+
* uint64
|
144
|
+
* How much space is available for VDisk corresponding to PDisk's hard space limits
|
145
|
+
*/
|
146
|
+
AvailableSize?: string;
|
147
|
+
/** Does this disk has some unreadable but not yet restored blobs? */
|
148
|
+
HasUnreadableBlobs?: boolean;
|
149
|
+
/** fixed64 */
|
150
|
+
IncarnationGuid?: string;
|
151
|
+
DonorMode?: boolean;
|
152
|
+
/**
|
153
|
+
* fixed64
|
154
|
+
* VDisk actor instance guid
|
155
|
+
*/
|
156
|
+
InstanceGuid?: string;
|
157
|
+
Donors?: TVDiskStateInfo[];
|
158
|
+
|
159
|
+
/** VDisk (Skeleton) Front Queue Status */
|
160
|
+
FrontQueues?: EFlag;
|
161
|
+
|
162
|
+
/** VDisk storage pool label */
|
163
|
+
StoragePoolName?: string;
|
164
|
+
|
165
|
+
/**
|
166
|
+
* uint64
|
167
|
+
* Read bytes per second from PDisk for TEvVGet blobs only
|
168
|
+
*/
|
169
|
+
ReadThroughput?: string;
|
170
|
+
/**
|
171
|
+
* uint64
|
172
|
+
* Write bytes per second to PDisk for TEvVPut blobs and replication bytes only
|
173
|
+
*/
|
174
|
+
WriteThroughput?: string;
|
175
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
export type RequiredField<Src, Fields extends keyof Src> = Src & Required<Pick<Src, Fields>>;
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "ydb-embedded-ui",
|
3
|
-
"version": "1.
|
3
|
+
"version": "1.11.0",
|
4
4
|
"files": [
|
5
5
|
"dist"
|
6
6
|
],
|
@@ -9,10 +9,6 @@
|
|
9
9
|
"url": "git@github.com:ydb-platform/ydb-embedded-ui.git"
|
10
10
|
},
|
11
11
|
"dependencies": {
|
12
|
-
"@testing-library/jest-dom": "5.15.0",
|
13
|
-
"@testing-library/react": "11.2.7",
|
14
|
-
"@testing-library/user-event": "12.8.3",
|
15
|
-
"@types/qs": "6.9.7",
|
16
12
|
"@yandex-cloud/i18n": "0.6.0",
|
17
13
|
"@yandex-cloud/paranoid": "1.0.0",
|
18
14
|
"@yandex-cloud/react-data-table": "0.2.1",
|
@@ -63,6 +59,27 @@
|
|
63
59
|
"eslint --fix --quiet"
|
64
60
|
]
|
65
61
|
},
|
62
|
+
"jest": {
|
63
|
+
"verbose": true,
|
64
|
+
"moduleFileExtensions": [
|
65
|
+
"js",
|
66
|
+
"json",
|
67
|
+
"ts",
|
68
|
+
"tsx"
|
69
|
+
],
|
70
|
+
"rootDir": ".",
|
71
|
+
"transform": {
|
72
|
+
"^.+\\.[jt]sx?$": "ts-jest"
|
73
|
+
},
|
74
|
+
"coverageDirectory": "./coverage",
|
75
|
+
"collectCoverageFrom": [
|
76
|
+
"src/**/*.{ts,tsx,js,jsx}"
|
77
|
+
],
|
78
|
+
"testEnvironment": "jsdom",
|
79
|
+
"moduleNameMapper": {
|
80
|
+
"\\.(css|less|scss|sass)$": "jest-transform-css"
|
81
|
+
}
|
82
|
+
},
|
66
83
|
"browserslist": {
|
67
84
|
"production": [
|
68
85
|
">0.2%",
|
@@ -78,7 +95,11 @@
|
|
78
95
|
"devDependencies": {
|
79
96
|
"@commitlint/cli": "^15.0.0",
|
80
97
|
"@commitlint/config-conventional": "^15.0.0",
|
98
|
+
"@testing-library/jest-dom": "^5.15.0",
|
99
|
+
"@testing-library/react": "^11.2.7",
|
100
|
+
"@testing-library/user-event": "^12.8.3",
|
81
101
|
"@types/lodash": "^4.14.178",
|
102
|
+
"@types/qs": "^6.9.7",
|
82
103
|
"@types/react": "^17.0.44",
|
83
104
|
"@types/react-dom": "^17.0.11",
|
84
105
|
"@types/react-router": "^5.1.17",
|
@@ -95,6 +116,7 @@
|
|
95
116
|
"copyfiles": "^2.4.1",
|
96
117
|
"eslint-config-prettier": "^8.3.0",
|
97
118
|
"husky": "^7.0.4",
|
119
|
+
"jest-transform-css": "^4.0.1",
|
98
120
|
"lint-staged": "^12.3.7",
|
99
121
|
"postcss": "^8.4.6",
|
100
122
|
"prettier": "^2.5.1",
|
@@ -102,6 +124,7 @@
|
|
102
124
|
"react-app-rewired": "^2.1.11",
|
103
125
|
"react-dom": "^17.0.2",
|
104
126
|
"stylelint": "^14.3.0",
|
127
|
+
"ts-jest": "^28.0.7",
|
105
128
|
"typescript": "^4.5.5"
|
106
129
|
},
|
107
130
|
"peerDependencies": {
|
@@ -1,47 +0,0 @@
|
|
1
|
-
import React from 'react';
|
2
|
-
import PropTypes from 'prop-types';
|
3
|
-
import cn from 'bem-cn-lite';
|
4
|
-
import './InfoViewer.scss';
|
5
|
-
|
6
|
-
const b = cn('info-viewer');
|
7
|
-
|
8
|
-
class InfoViewer extends React.Component {
|
9
|
-
render() {
|
10
|
-
const {info, className, title} = this.props;
|
11
|
-
|
12
|
-
return (
|
13
|
-
<div className={`${b()} ${className}`}>
|
14
|
-
{title && <div className={b('title')}>{title}</div>}
|
15
|
-
{info && info.length > 0 ? (
|
16
|
-
<div className={b('items')}>
|
17
|
-
{info.map((data, infoIndex) => (
|
18
|
-
<div className={b('row')} key={infoIndex}>
|
19
|
-
<div className={b('label')}>
|
20
|
-
{data.label}
|
21
|
-
<div className={b('dots')}></div>
|
22
|
-
</div>
|
23
|
-
|
24
|
-
<div className={b('value')}>{data.value}</div>
|
25
|
-
</div>
|
26
|
-
))}
|
27
|
-
</div>
|
28
|
-
) : (
|
29
|
-
<div>no {title} data</div>
|
30
|
-
)}
|
31
|
-
</div>
|
32
|
-
);
|
33
|
-
}
|
34
|
-
}
|
35
|
-
|
36
|
-
InfoViewer.propTypes = {
|
37
|
-
className: PropTypes.string,
|
38
|
-
info: PropTypes.array.isRequired,
|
39
|
-
title: PropTypes.string,
|
40
|
-
dots: PropTypes.bool,
|
41
|
-
};
|
42
|
-
|
43
|
-
InfoViewer.defaultProps = {
|
44
|
-
className: '',
|
45
|
-
};
|
46
|
-
|
47
|
-
export default InfoViewer;
|