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 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<{label?: string, value?: unknown}> = [];
37
+ const info: Array<InfoViewerItem> = [];
38
38
 
39
39
  let key: keyof TIndexDescription;
40
40
  for (key in TableIndex) {
@@ -1,18 +1,23 @@
1
1
  .info-viewer {
2
- font-size: var(--yc-text-body-2-font-size);
3
- line-height: var(--yc-text-body-2-line-height);
2
+ --ydb-info-viewer-font-size: var(--yc-text-body-2-font-size);
3
+ --ydb-info-viewer-line-height: var(--yc-text-body-2-line-height);
4
+ --ydb-info-viewer-title-font-weight: 600;
5
+ --ydb-info-viewer-title-margin: 15px 0 10px;
6
+ --ydb-info-viewer-items-gap: 7px;
7
+
8
+ font-size: var(--ydb-info-viewer-font-size);
9
+ line-height: var(--ydb-info-viewer-line-height);
10
+
4
11
  &__title {
5
- margin: 15px 0 10px;
12
+ margin: var(--ydb-info-viewer-title-margin);
6
13
 
7
- font-size: var(--yc-text-body-2-font-size);
8
- font-weight: 600;
9
- line-height: var(--yc-text-body-2-line-height);
14
+ font-weight: var(--ydb-info-viewer-title-font-weight);
10
15
  }
11
16
 
12
17
  &__items {
13
18
  display: flex;
14
19
  flex-direction: column;
15
- gap: 7px;
20
+ gap: var(--ydb-info-viewer-items-gap);
16
21
 
17
22
  max-width: 100%;
18
23
  }
@@ -51,4 +56,24 @@
51
56
 
52
57
  white-space: nowrap;
53
58
  }
59
+
60
+ &_size {
61
+ &_s {
62
+ --ydb-info-viewer-font-size: var(--yc-text-body-1-font-size);
63
+ --ydb-info-viewer-line-height: var(--yc-text-body-1-line-height);
64
+ --ydb-info-viewer-title-font-weight: 500;
65
+ --ydb-info-viewer-title-margin: 0 0 4px;
66
+ --ydb-info-viewer-items-gap: 4px;
67
+
68
+ .info-viewer {
69
+ &__row {
70
+ height: auto;
71
+ }
72
+
73
+ &__label {
74
+ min-width: 85px;
75
+ }
76
+ }
77
+ }
78
+ }
54
79
  }
@@ -0,0 +1,43 @@
1
+ import type {ReactNode} from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ import './InfoViewer.scss';
5
+
6
+ export interface InfoViewerItem {
7
+ label: string;
8
+ value: ReactNode;
9
+ }
10
+
11
+ interface InfoViewerProps {
12
+ title?: string;
13
+ info?: InfoViewerItem[];
14
+ dots?: boolean;
15
+ size?: 's';
16
+ className?: string;
17
+ }
18
+
19
+ const b = cn('info-viewer');
20
+
21
+ const InfoViewer = ({title, info, dots = true, size, className}: InfoViewerProps) => (
22
+ <div className={b({size}, className)}>
23
+ {title && <div className={b('title')}>{title}</div>}
24
+ {info && info.length > 0 ? (
25
+ <div className={b('items')}>
26
+ {info.map((data, infoIndex) => (
27
+ <div className={b('row')} key={data.label + infoIndex}>
28
+ <div className={b('label')}>
29
+ {data.label}
30
+ {dots && <div className={b('dots')} />}
31
+ </div>
32
+
33
+ <div className={b('value')}>{data.value}</div>
34
+ </div>
35
+ ))}
36
+ </div>
37
+ ) : (
38
+ <>no {title} data</>
39
+ )}
40
+ </div>
41
+ );
42
+
43
+ export default InfoViewer;
@@ -2,3 +2,4 @@ import InfoViewer from './InfoViewer';
2
2
 
3
3
  export {InfoViewer};
4
4
  export * from './utils';
5
+ export type {InfoViewerItem} from './InfoViewer';
@@ -1,9 +1,11 @@
1
+ import type {ReactNode} from "react";
2
+
1
3
  type LabelMap<T> = {
2
4
  [label in keyof T]?: string;
3
5
  }
4
6
 
5
7
  type ValueFormatters<T> = {
6
- [label in keyof T]?: (value: T[label]) => string | undefined;
8
+ [label in keyof T]?: (value: T[label]) => ReactNode;
7
9
  }
8
10
 
9
11
  function formatLabel<Shape>(label: keyof Shape, map: LabelMap<Shape>) {
@@ -14,18 +16,18 @@ function formatValue<Shape, Key extends keyof Shape>(
14
16
  label: Key,
15
17
  value: Shape[Key],
16
18
  formatters: ValueFormatters<Shape>,
17
- defaultFormatter?: (value: Shape[Key]) => string | undefined,
19
+ defaultFormatter?: (value: Shape[Key]) => ReactNode,
18
20
  ) {
19
21
  const formatter = formatters[label] || defaultFormatter;
20
22
  const formattedValue = formatter ? formatter(value) : value;
21
23
 
22
- return String(formattedValue ?? '');
24
+ return formattedValue;
23
25
  }
24
26
 
25
27
  interface CreateInfoFormatterOptions<Shape> {
26
28
  values?: ValueFormatters<Shape>,
27
29
  labels?: LabelMap<Shape>,
28
- defaultValueFormatter?: (value: Shape[keyof Shape]) => string | undefined,
30
+ defaultValueFormatter?: (value: Shape[keyof Shape]) => ReactNode,
29
31
  }
30
32
 
31
33
  export function createInfoFormatter<Shape extends Record<string, any>>({
@@ -0,0 +1,55 @@
1
+ .stack {
2
+ --ydb-stack-base-z-index: 100;
3
+ --ydb-stack-offset-x: 4px;
4
+ --ydb-stack-offset-y: 4px;
5
+ --ydb-stack-offset-x-hover: 4px;
6
+ --ydb-stack-offset-y-hover: 8px;
7
+
8
+ position: relative;
9
+
10
+ &__layer {
11
+ transition: transform 0.1s ease-out;
12
+
13
+ &:first-child {
14
+ position: relative;
15
+ z-index: var(--ydb-stack-base-z-index);
16
+ }
17
+
18
+ & + & {
19
+ position: absolute;
20
+ z-index: calc(var(--ydb-stack-base-z-index) - var(--ydb-stack-level));
21
+ top: 0;
22
+ left: 0;
23
+
24
+ width: 100%;
25
+ height: 100%;
26
+
27
+ transform: translate(
28
+ calc(var(--ydb-stack-level) * var(--ydb-stack-offset-x)),
29
+ calc(var(--ydb-stack-level) * var(--ydb-stack-offset-y))
30
+ );
31
+ }
32
+ }
33
+
34
+ &:hover {
35
+ .stack__layer:first-child {
36
+ transform: translate(
37
+ calc(-1 * var(--ydb-stack-offset-x-hover)),
38
+ calc(-1 * var(--ydb-stack-offset-y-hover))
39
+ );
40
+ }
41
+
42
+ .stack__layer + .stack__layer {
43
+ transform: translate(
44
+ calc(
45
+ var(--ydb-stack-level) * (var(--ydb-stack-offset-x-hover) * 2) -
46
+ var(--ydb-stack-offset-x-hover)
47
+ ),
48
+ calc(
49
+ var(--ydb-stack-level) * (var(--ydb-stack-offset-y-hover) * 2) -
50
+ var(--ydb-stack-offset-y-hover)
51
+ )
52
+ );
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ import './Stack.scss';
5
+
6
+ interface StackProps {
7
+ className?: string;
8
+ }
9
+
10
+ const LAYER_CSS_VAR = '--ydb-stack-level';
11
+
12
+ const b = cn('stack');
13
+
14
+ export const Stack: React.FC<StackProps> = ({children, className}) => (
15
+ <div className={b(null, className)}>
16
+ {
17
+ React.Children.map(children, (child, index) => {
18
+ if (!React.isValidElement(child)) {
19
+ return null;
20
+ }
21
+
22
+ return (
23
+ <div
24
+ className={b('layer')}
25
+ style={{
26
+ [LAYER_CSS_VAR]: index,
27
+ } as React.CSSProperties}
28
+ >
29
+ {child}
30
+ </div>
31
+ );
32
+ })
33
+ }
34
+ </div>
35
+ );
@@ -7,6 +7,8 @@
7
7
  width: 100%;
8
8
  height: var(--yc-text-body-2-line-height);
9
9
 
10
+ vertical-align: top;
11
+
10
12
  border: 1px solid var(--yc-color-infographics-misc-medium);
11
13
  border-radius: 2px;
12
14
  background-color: var(--yc-color-infographics-misc-light);
@@ -46,6 +46,11 @@ function DiskStateProgressBar({
46
46
  ? b({[diskProgressColors[severity].toLowerCase()]: true})
47
47
  : undefined
48
48
  }
49
+ role="meter"
50
+ aria-label="Disk allocated space"
51
+ aria-valuemin={0}
52
+ aria-valuemax={100}
53
+ aria-valuenow={diskAllocatedPercent}
49
54
  >
50
55
  {href ? (
51
56
  <InternalLink to={href} className={b('link')}>
@@ -11,25 +11,8 @@
11
11
  &:last-child {
12
12
  margin-right: 0px;
13
13
  }
14
- &__popup-wrapper {
15
- padding: 5px 10px;
16
- }
17
- &__popup-content {
18
- display: grid;
19
- justify-items: stretch;
20
- column-gap: 5px;
21
- }
22
- &__popup-section-name {
23
- grid-column: 1 / 3;
24
-
25
- margin: 5px 0;
26
14
 
27
- font-weight: 500;
28
- text-align: center;
29
-
30
- border-bottom: 1px solid var(--yc-color-line-generic);
31
- }
32
- &__property {
33
- text-align: right;
15
+ &__popup-wrapper {
16
+ padding: 12px;
34
17
  }
35
18
  }
@@ -1,7 +1,8 @@
1
1
  import React, {useEffect, useState, useRef, useMemo} from 'react';
2
2
  import cn from 'bem-cn-lite';
3
- import _ from 'lodash';
4
3
  import {Popup} from '@yandex-cloud/uikit';
4
+
5
+ import type {RequiredField} from '../../../types';
5
6
  //@ts-ignore
6
7
  import {bytesToGB} from '../../../utils/utils';
7
8
  //@ts-ignore
@@ -10,6 +11,7 @@ import routes, {createHref} from '../../../routes';
10
11
  import {getPDiskId} from '../../../utils';
11
12
  import {getPDiskType} from '../../../utils/pdisk';
12
13
  import {TPDiskStateInfo, TPDiskState} from '../../../types/api/storage';
14
+ import {InfoViewer} from '../../../components/InfoViewer';
13
15
  import DiskStateProgressBar, {
14
16
  diskProgressColors,
15
17
  } from '../DiskStateProgressBar/DiskStateProgressBar';
@@ -38,7 +40,7 @@ const stateSeverity = {
38
40
  [TPDiskState.DeviceIoError]: 5,
39
41
  };
40
42
 
41
- type PDiskProps = TPDiskStateInfo;
43
+ type PDiskProps = RequiredField<TPDiskStateInfo, 'NodeId'>;
42
44
 
43
45
  const isSeverityKey = (key?: TPDiskState): key is keyof typeof stateSeverity =>
44
46
  key !== undefined && key in stateSeverity;
@@ -76,51 +78,46 @@ function Pdisk(props: PDiskProps) {
76
78
  diskProgressColors[colorSeverity.Yellow as keyof typeof diskProgressColors],
77
79
  ];
78
80
 
79
- const pdiskData: {property: string; value: string | number}[] = [
80
- {property: 'PDisk', value: getPDiskId({NodeId, PDiskId})},
81
+ const pdiskData: {label: string; value: string | number}[] = [
82
+ {label: 'PDisk', value: getPDiskId({NodeId, PDiskId})},
81
83
  ];
82
84
 
83
- pdiskData.push({property: 'State', value: State || 'not available'});
84
- pdiskData.push({property: 'Type', value: getPDiskType(props) || 'unknown'});
85
- NodeId && pdiskData.push({property: 'Node Id', value: NodeId});
85
+ pdiskData.push({label: 'State', value: State || 'not available'});
86
+ pdiskData.push({label: 'Type', value: getPDiskType(props) || 'unknown'});
87
+ NodeId && pdiskData.push({label: 'Node Id', value: NodeId});
86
88
 
87
- Path && pdiskData.push({property: 'Path', value: Path});
89
+ Path && pdiskData.push({label: 'Path', value: Path});
88
90
  pdiskData.push({
89
- property: 'Available',
91
+ label: 'Available',
90
92
  value: `${bytesToGB(AvailableSize)} of ${bytesToGB(TotalSize)}`,
91
93
  });
92
94
  Realtime &&
93
95
  errorColors.includes(Realtime) &&
94
- pdiskData.push({property: 'Realtime', value: Realtime});
96
+ pdiskData.push({label: 'Realtime', value: Realtime});
95
97
  Device &&
96
98
  errorColors.includes(Device) &&
97
- pdiskData.push({property: 'Device', value: Device});
99
+ pdiskData.push({label: 'Device', value: Device});
98
100
  return pdiskData;
99
101
  };
100
102
  /* eslint-enable */
101
103
 
102
- const renderPopup = () => {
103
- const pdiskData = preparePdiskData();
104
- return (
105
- <Popup
106
- className={b('popup-wrapper')}
107
- anchorRef={anchor}
108
- open={isPopupVisible}
109
- placement={['top', 'bottom']}
110
- hasArrow
111
- >
112
- <div className={b('popup-content')}>
113
- <div className={b('popup-section-name')}>PDisk</div>
114
- {_.map(pdiskData, (row) => (
115
- <React.Fragment key={row.property}>
116
- <div className={b('property')}>{row.property}</div>
117
- <div className={b('value')}>{row.value}</div>
118
- </React.Fragment>
119
- ))}
120
- </div>
121
- </Popup>
122
- );
123
- };
104
+ const renderPopup = () => (
105
+ <Popup
106
+ className={b('popup-wrapper')}
107
+ anchorRef={anchor}
108
+ open={isPopupVisible}
109
+ placement={['top', 'bottom']}
110
+ // bigger offset for easier switching to neighbour nodes
111
+ // matches the default offset for popup with arrow out of a sense of beauty
112
+ offset={[0, 12]}
113
+ >
114
+ <InfoViewer
115
+ title="PDisk"
116
+ info={preparePdiskData()}
117
+ size="s"
118
+ />
119
+ </Popup>
120
+ );
124
121
 
125
122
  const pdiskAllocatedPercent = useMemo(() => {
126
123
  const {AvailableSize, TotalSize} = props;
@@ -0,0 +1,40 @@
1
+ import {render} from '@testing-library/react'
2
+ import {MemoryRouter} from 'react-router-dom';
3
+
4
+ import {TPDiskState} from '../../../../types/api/storage'
5
+
6
+ import PDisk from '../Pdisk'
7
+
8
+ describe('PDisk state', () => {
9
+ it('Should determine severity based on State', () => {
10
+ const {getAllByRole} = render(
11
+ <MemoryRouter>
12
+ <PDisk
13
+ NodeId={1}
14
+ State={TPDiskState.Normal}
15
+ />
16
+ <PDisk
17
+ NodeId={2}
18
+ State={TPDiskState.ChunkQuotaError}
19
+ />
20
+ </MemoryRouter>
21
+ );
22
+
23
+ const [normalDisk, erroredDisk] = getAllByRole('meter');
24
+
25
+ expect(normalDisk.className).not.toBe(erroredDisk.className);
26
+ });
27
+
28
+ it('Should display as unavailabe when no State is provided', () => {
29
+ const {getByRole} = render(
30
+ <MemoryRouter>
31
+ <PDisk NodeId={1} />
32
+ </MemoryRouter>
33
+ );
34
+
35
+ const disk = getByRole('meter');
36
+
37
+ // unavailable disks display with the highest severity
38
+ expect(disk.className).toMatch(/_red\b/i);
39
+ });
40
+ });
@@ -1,23 +1,45 @@
1
1
  .global-storage-groups {
2
+ &__vdisks-column {
3
+ overflow: visible; // to enable stacked disks overflow the row
4
+ }
5
+
2
6
  &__vdisks-wrapper {
3
7
  display: flex;
4
- overflow-x: auto;
5
- overflow-y: hidden;
6
8
  justify-content: center;
7
9
 
8
10
  min-width: 500px;
9
11
  }
12
+ &__vdisks-item {
13
+ flex-grow: 1;
14
+
15
+ max-width: 200px;
16
+ margin-right: 10px;
17
+
18
+ &:last-child {
19
+ margin-right: 0px;
20
+ }
21
+
22
+ .stack__layer {
23
+ background: var(--yc-color-base-background);
24
+
25
+ .data-table__row:hover & {
26
+ background: var(--yc-color-base-float-hover);
27
+ }
28
+ }
29
+ }
10
30
  &__pool-name-wrapper {
11
31
  display: flex;
12
32
  align-items: flex-end;
33
+
34
+ width: 230px;
13
35
  }
14
36
  &__pool-name {
15
37
  display: inline-block;
16
38
  overflow: hidden;
17
39
 
18
- width: 230px;
19
40
  max-width: 230px;
20
41
 
42
+ vertical-align: top;
21
43
  text-overflow: ellipsis;
22
44
  }
23
45
  &__group-id {
@@ -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
  ),
@@ -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 _ from 'lodash';
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
- if (!Replicated && newSeverity === colorSeverity.Green) {
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 = [{property: 'VDisk', value: stringifyVdiskId(VDiskId)}];
99
- vdiskData.push({property: 'State', value: VDiskState ?? 'not available'});
100
- PoolName && vdiskData.push({property: 'StoragePool', value: PoolName});
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
- property: 'Fresh',
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
- property: 'Level',
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
- property: 'Fresh',
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
- property: 'Level',
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({property: 'Space', value: DiskSpace});
138
+ vdiskData.push({label: 'Space', value: DiskSpace});
133
139
 
134
140
  FrontQueues &&
135
141
  FrontQueues !== diskProgressColors[colorSeverity.Green] &&
136
- vdiskData.push({property: 'FrontQueues', value: FrontQueues});
142
+ vdiskData.push({label: 'FrontQueues', value: FrontQueues});
137
143
 
138
- !Replicated && vdiskData.push({property: 'Replicated', value: 'NO'});
144
+ !Replicated && vdiskData.push({label: 'Replicated', value: 'NO'});
139
145
 
140
- UnsyncedVDisks && vdiskData.push({property: 'UnsyncVDisks', value: UnsyncedVDisks});
146
+ UnsyncedVDisks && vdiskData.push({label: 'UnsyncVDisks', value: UnsyncedVDisks});
141
147
 
142
148
  Boolean(Number(AllocatedSize)) &&
143
149
  vdiskData.push({
144
- property: 'Allocated',
150
+ label: 'Allocated',
145
151
  value: bytesToGB(AllocatedSize),
146
152
  });
147
153
 
148
154
  Boolean(Number(ReadThroughput)) &&
149
- vdiskData.push({property: 'Read', value: bytesToSpeed(ReadThroughput)});
155
+ vdiskData.push({label: 'Read', value: bytesToSpeed(ReadThroughput)});
150
156
 
151
157
  Boolean(Number(WriteThroughput)) &&
152
158
  vdiskData.push({
153
- property: 'Write',
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 = [{property: 'PDisk', value: getPDiskId(PDisk)}];
174
+ const pdiskData = [{label: 'PDisk', value: getPDiskId(PDisk)}];
169
175
  pdiskData.push({
170
- property: 'State',
176
+ label: 'State',
171
177
  value: PDisk.State || 'not available',
172
178
  });
173
- pdiskData.push({property: 'Type', value: getPDiskType(PDisk) || 'unknown'});
174
- PDisk.NodeId && pdiskData.push({property: 'Node Id', value: PDisk.NodeId});
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({property: 'Host', value: nodes[PDisk.NodeId]});
178
- PDisk.Path && pdiskData.push({property: 'Path', value: PDisk.Path});
183
+ pdiskData.push({label: 'Host', value: nodes[PDisk.NodeId]});
184
+ PDisk.Path && pdiskData.push({label: 'Path', value: PDisk.Path});
179
185
  pdiskData.push({
180
- property: 'Available',
186
+ label: 'Available',
181
187
  value: `${bytesToGB(PDisk.AvailableSize)} of ${bytesToGB(PDisk.TotalSize)}`,
182
188
  });
183
189
  errorColors.includes(PDisk.Realtime) &&
184
- pdiskData.push({property: 'Realtime', value: PDisk.Realtime});
190
+ pdiskData.push({label: 'Realtime', value: PDisk.Realtime});
185
191
  errorColors.includes(PDisk.Device) &&
186
- pdiskData.push({property: 'Device', value: PDisk.Device});
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
- const vdiskData = prepareVdiskData();
195
- const pdiskData = preparePdiskData();
196
- return (
197
- <Popup
198
- className={b('popup-wrapper')}
199
- anchorRef={anchor}
200
- open={isPopupVisible}
201
- placement={['top', 'bottom']}
202
- hasArrow
203
- >
204
- <div className={b('popup-content')}>
205
- <div className={b('popup-section-name')}>VDisk</div>
206
- {_.map(vdiskData, (row) => (
207
- <React.Fragment key={row.property}>
208
- <div className={b('property')}>{row.property}</div>
209
- <div className={b('value')}>{row.value}</div>
210
- </React.Fragment>
211
- ))}
212
- <div className={b('popup-section-name')}>PDisk</div>
213
- {_.map(pdiskData, (row) => (
214
- <React.Fragment key={row.property}>
215
- <div className={b('property')}>{row.property}</div>
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
- routes.node,
247
- {id: props.NodeId, activeTab: STRUCTURE},
248
- {
249
- pdiskId: props.PDisk?.PDiskId,
250
- vdiskId: stringifyVdiskId(props.VDiskId),
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: 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;
3
+ padding: 12px;
24
4
 
25
- margin: 5px 0;
5
+ .info-viewer + .info-viewer {
6
+ margin-top: 8px;
7
+ padding-top: 8px;
26
8
 
27
- font-weight: 500;
28
- text-align: center;
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
- &__property {
33
- text-align: right;
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
+ });
@@ -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.10.3",
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;
@@ -1,5 +0,0 @@
1
- describe('Fake tests', () => {
2
- it('A fake test', () => {
3
- expect(true).toBe(true);
4
- });
5
- });