ydb-embedded-ui 1.8.8 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/components/BasicNodeViewer/BasicNodeViewer.scss +43 -0
  3. package/dist/components/BasicNodeViewer/BasicNodeViewer.tsx +53 -0
  4. package/dist/components/BasicNodeViewer/index.ts +1 -0
  5. package/dist/components/EntityStatus/EntityStatus.js +15 -3
  6. package/dist/components/FullNodeViewer/FullNodeViewer.js +29 -48
  7. package/dist/components/FullNodeViewer/FullNodeViewer.scss +0 -45
  8. package/dist/components/IndexInfoViewer/IndexInfoViewer.tsx +52 -0
  9. package/dist/components/InfoViewer/index.ts +4 -0
  10. package/dist/components/InfoViewer/utils.ts +32 -0
  11. package/dist/components/ProgressViewer/ProgressViewer.js +1 -1
  12. package/dist/containers/Node/Node.scss +5 -1
  13. package/dist/containers/Node/Node.tsx +7 -1
  14. package/dist/containers/Node/NodeOverview/NodeOverview.tsx +1 -3
  15. package/dist/containers/Node/NodeStructure/NodeStructure.scss +30 -1
  16. package/dist/containers/Node/NodeStructure/PDiskTitleBadge.tsx +25 -0
  17. package/dist/containers/Node/NodeStructure/Pdisk.tsx +24 -2
  18. package/dist/containers/Nodes/Nodes.js +1 -0
  19. package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +16 -2
  20. package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.js +20 -16
  21. package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +1 -0
  22. package/dist/types/api/schema.ts +36 -0
  23. package/dist/types/api/storage.ts +54 -0
  24. package/dist/utils/getNodesColumns.js +2 -0
  25. package/dist/utils/pdisk.ts +74 -0
  26. package/dist/utils/tooltip.js +27 -0
  27. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.9.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.8.8...v1.9.0) (2022-07-29)
4
+
5
+
6
+ ### Features
7
+
8
+ * **Node:** display endpoints in overview ([89e9e47](https://github.com/ydb-platform/ydb-embedded-ui/commit/89e9e470499b6f458e8949211d97293c0b7d9b97))
9
+ * **Node:** display node basic info above tabs ([aafb15b](https://github.com/ydb-platform/ydb-embedded-ui/commit/aafb15b399bf116026eff36f3c4ac817e2c40e18))
10
+ * **Node:** more informative pdisks panels ([342712b](https://github.com/ydb-platform/ydb-embedded-ui/commit/342712bcaa793971e1ca354da57fb962639ef90c))
11
+ * **Nodes:** show node endpoints in tooltip ([34be559](https://github.com/ydb-platform/ydb-embedded-ui/commit/34be55957e02f947ede30b43f22fde82d21df308))
12
+ * **Tenant:** table index overview ([2aed714](https://github.com/ydb-platform/ydb-embedded-ui/commit/2aed71488cde1175e6569c236ab609bb126f9cf3))
13
+ * **Tenant:** virtualized tree in schema ([815f558](https://github.com/ydb-platform/ydb-embedded-ui/commit/815f5588e5fed6fb86f69653c4937e975465372f))
14
+ * utils for parsing bitfields in pdisk data ([da22b4a](https://github.com/ydb-platform/ydb-embedded-ui/commit/da22b4afde9efe4d9605cefb69ddd51aed989722))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * **Node:** fix pdisk title items width ([ca5fec6](https://github.com/ydb-platform/ydb-embedded-ui/commit/ca5fec6388364b7d1d6362f1bda36431d9c29749))
20
+ * **Nodes:** hide tooltip on unmount ([54e4fdc](https://github.com/ydb-platform/ydb-embedded-ui/commit/54e4fdc8045c555338e79d89a93faf58e888fa0e))
21
+ * **ProgressViewer:** apply provided custom class name ([aa60e9d](https://github.com/ydb-platform/ydb-embedded-ui/commit/aa60e9d1b9c0752853f4323d3bcfd220bedd272d))
22
+ * **Tenant:** display all table props in overview ([d70e311](https://github.com/ydb-platform/ydb-embedded-ui/commit/d70e311296f6a4d1781f6e72929c70e0db7c3226))
23
+ * **Tenant:** display PartCount first in table overview ([8c09746](https://github.com/ydb-platform/ydb-embedded-ui/commit/8c09746b026a23a36fe31be94057cc92535aceaa))
24
+
3
25
  ## [1.8.8](https://github.com/ydb-platform/ydb-embedded-ui/compare/v1.8.7...v1.8.8) (2022-07-21)
4
26
 
5
27
 
@@ -0,0 +1,43 @@
1
+ @import '../../styles/mixins.scss';
2
+
3
+ .basic-node-viewer {
4
+ font-size: var(--yc-text-body-2-font-size);
5
+ line-height: var(--yc-text-body-2-line-height);
6
+
7
+ display: flex;
8
+ align-items: center;
9
+
10
+ margin: 15px 0;
11
+
12
+ &__title {
13
+ margin: 0 20px 0 0;
14
+
15
+ font-size: var(--yc-text-body-2-font-size);
16
+ font-weight: 600;
17
+ line-height: var(--yc-text-body-2-line-height);
18
+ text-transform: uppercase;
19
+ }
20
+
21
+ &__id {
22
+ margin: 0 15px 0 24px;
23
+ }
24
+
25
+ &__label {
26
+ margin-right: 10px;
27
+
28
+ font-size: var(--yc-text-body-2-font-size);
29
+ line-height: 18px;
30
+ white-space: nowrap;
31
+
32
+ color: var(--yc-color-text-hint);
33
+
34
+ .yc-root_theme_dark & {
35
+ color: var(--yc-color-text-hint);
36
+ }
37
+ }
38
+
39
+ &__link {
40
+ margin-left: 5px;
41
+ @extend .link;
42
+ }
43
+ }
@@ -0,0 +1,53 @@
1
+ import cn from 'bem-cn-lite';
2
+
3
+ import EntityStatus from '../EntityStatus/EntityStatus';
4
+ import Tags from '../Tags/Tags';
5
+ import Icon from '../Icon/Icon';
6
+
7
+ import './BasicNodeViewer.scss';
8
+
9
+ const b = cn('basic-node-viewer');
10
+
11
+ interface BasicNodeViewerProps {
12
+ node: any;
13
+ additionalNodesInfo?: any;
14
+ className?: string;
15
+ }
16
+
17
+ export const BasicNodeViewer = ({node, additionalNodesInfo, className}: BasicNodeViewerProps) => {
18
+ const nodeHref = additionalNodesInfo?.getNodeRef
19
+ ? additionalNodesInfo.getNodeRef(node) + 'internal'
20
+ : undefined;
21
+
22
+ return (
23
+ <div className={b(null, className)}>
24
+ {node ? (
25
+ <>
26
+ <div className={b('title')}>Node</div>
27
+ <EntityStatus status={node.SystemState} name={node.Host} />
28
+ {nodeHref && (
29
+ <a
30
+ rel="noopener noreferrer"
31
+ className={b('link', {external: true})}
32
+ href={nodeHref}
33
+ target="_blank"
34
+ >
35
+ <Icon name="external" />
36
+ </a>
37
+ )}
38
+
39
+ <div className={b('id')}>
40
+ <label className={b('label')}>NodeID</label>
41
+ <label>{node.NodeId}</label>
42
+ </div>
43
+
44
+ <Tags tags={[node.DataCenter]} />
45
+ <Tags tags={node.Roles} tagsType="blue" />
46
+ </>
47
+ ) : (
48
+ <div className="error">no data</div>
49
+ )}
50
+
51
+ </div>
52
+ );
53
+ };
@@ -0,0 +1 @@
1
+ export * from './BasicNodeViewer';
@@ -12,6 +12,8 @@ class EntityStatus extends React.Component {
12
12
  static propTypes = {
13
13
  status: PropTypes.string,
14
14
  name: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
15
+ onNameMouseEnter: PropTypes.func,
16
+ onNameMouseLeave: PropTypes.func,
15
17
  path: PropTypes.string,
16
18
  size: PropTypes.string,
17
19
  label: PropTypes.string,
@@ -49,19 +51,29 @@ class EntityStatus extends React.Component {
49
51
  );
50
52
  }
51
53
  renderLink() {
52
- const {externalLink, name, path} = this.props;
54
+ const {externalLink, name, path, onNameMouseEnter, onNameMouseLeave} = this.props;
53
55
 
54
56
  if (externalLink) {
55
57
  return <ExternalLink href={path}>{name}</ExternalLink>;
56
58
  }
57
59
 
58
60
  return path ? (
59
- <Link title={name} to={path}>
61
+ <Link
62
+ title={name}
63
+ to={path}
64
+ onMouseEnter={onNameMouseEnter}
65
+ onMouseLeave={onNameMouseLeave}
66
+ >
60
67
  {name}
61
68
  </Link>
62
69
  ) : (
63
70
  name && (
64
- <span className={b('name')} title={name}>
71
+ <span
72
+ className={b('name')}
73
+ title={name}
74
+ onMouseEnter={onNameMouseEnter}
75
+ onMouseLeave={onNameMouseLeave}
76
+ >
65
77
  {name}
66
78
  </span>
67
79
  )
@@ -3,11 +3,8 @@ import cn from 'bem-cn-lite';
3
3
  import PropTypes from 'prop-types';
4
4
 
5
5
  import InfoViewer from '../InfoViewer/InfoViewer';
6
- import EntityStatus from '../EntityStatus/EntityStatus';
7
6
  import ProgressViewer from '../ProgressViewer/ProgressViewer';
8
7
  import PoolUsage from '../PoolUsage/PoolUsage';
9
- import Tags from '../Tags/Tags';
10
- import Icon from '../Icon/Icon';
11
8
 
12
9
  import {LOAD_AVERAGE_TIME_INTERVALS} from '../../utils/constants';
13
10
  import {calcUptime} from '../../utils';
@@ -22,7 +19,6 @@ class FullNodeViewer extends React.Component {
22
19
  node: PropTypes.object.isRequired,
23
20
  backend: PropTypes.string,
24
21
  singleClusterMode: PropTypes.bool,
25
- additionalNodesInfo: PropTypes.object,
26
22
  };
27
23
 
28
24
  static defaultProps = {
@@ -30,10 +26,12 @@ class FullNodeViewer extends React.Component {
30
26
  };
31
27
 
32
28
  render() {
33
- const {node, className, additionalNodesInfo={}} = this.props;
34
- const nodeHref = additionalNodesInfo.getNodeRef
35
- ? additionalNodesInfo.getNodeRef(node) + 'internal'
36
- : undefined;
29
+ const {node, className} = this.props;
30
+
31
+ const endpointsInfo = node.Endpoints?.map(({Name, Address}) => ({
32
+ label: Name,
33
+ value: Address,
34
+ }));
37
35
 
38
36
  const commonInfo = [
39
37
  {label: 'Version', value: node.Version},
@@ -50,52 +48,35 @@ class FullNodeViewer extends React.Component {
50
48
  return (
51
49
  <div className={`${b()} ${className}`}>
52
50
  {node ? (
53
- <div>
54
- <div className={b('row')}>
55
- <div className={b('title')}>Node</div>
56
- <EntityStatus status={node.SystemState} name={node.Host} />
57
- {nodeHref && (
58
- <a
59
- rel="noopener noreferrer"
60
- className={b('link', {external: true})}
61
- href={nodeHref}
62
- target="_blank"
63
- >
64
- <Icon name="external" />
65
- </a>
66
- )}
67
-
68
- <div className={b('row', {id: true})}>
69
- <label className={b('label', {id: true})}>NodeID</label>
70
- <label>{node.NodeId}</label>
51
+ <div className={b('common-info')}>
52
+ <div>
53
+ <div className={b('section-title')}>Pools</div>
54
+ <div className={b('section', {pools: true})}>
55
+ {node.PoolStats.map((pool, poolIndex) => (
56
+ <PoolUsage key={poolIndex} data={pool} />
57
+ ))}
71
58
  </div>
72
-
73
- <Tags tags={[node.DataCenter]} />
74
- <Tags tags={node.Roles} tagsType="blue" />
75
59
  </div>
76
60
 
77
- <div className={b('common-info')}>
78
- <div>
79
- <div className={b('section-title')}>Pools</div>
80
- <div className={b('section', {pools: true})}>
81
- {node.PoolStats.map((pool, poolIndex) => (
82
- <PoolUsage key={poolIndex} data={pool} />
83
- ))}
84
- </div>
85
- </div>
86
-
61
+ {endpointsInfo && endpointsInfo.length && (
87
62
  <InfoViewer
88
- title="Common info"
63
+ title="Endpoints"
89
64
  className={b('section')}
90
- info={commonInfo}
65
+ info={endpointsInfo}
91
66
  />
92
-
93
- <InfoViewer
94
- title="Load average"
95
- className={b('section', {average: true})}
96
- info={averageInfo}
97
- />
98
- </div>
67
+ )}
68
+
69
+ <InfoViewer
70
+ title="Common info"
71
+ className={b('section')}
72
+ info={commonInfo}
73
+ />
74
+
75
+ <InfoViewer
76
+ title="Load average"
77
+ className={b('section', {average: true})}
78
+ info={averageInfo}
79
+ />
99
80
  </div>
100
81
  ) : (
101
82
  <div className="error">no data</div>
@@ -4,26 +4,6 @@
4
4
  font-size: var(--yc-text-body-2-font-size);
5
5
  line-height: var(--yc-text-body-2-line-height);
6
6
 
7
- &__title {
8
- margin: 0 20px 0 0;
9
-
10
- font-size: var(--yc-text-body-2-font-size);
11
- font-weight: 600;
12
- line-height: var(--yc-text-body-2-line-height);
13
- text-transform: uppercase;
14
- }
15
-
16
- &__row {
17
- display: flex;
18
- align-items: center;
19
-
20
- margin: 15px 0;
21
-
22
- &_id {
23
- margin: 0 15px 0 24px;
24
- }
25
- }
26
-
27
7
  &__common-info {
28
8
  display: flex;
29
9
  flex-direction: column;
@@ -46,26 +26,6 @@
46
26
  min-width: 60px;
47
27
  }
48
28
 
49
- &__label {
50
- min-width: 100px;
51
- margin-right: 25px;
52
-
53
- font-size: var(--yc-text-body-2-font-size);
54
- line-height: 18px;
55
- white-space: nowrap;
56
-
57
- color: var(--yc-color-text-hint);
58
-
59
- .yc-root_theme_dark & {
60
- color: var(--yc-color-text-hint);
61
- }
62
-
63
- &_id {
64
- min-width: auto;
65
- margin-right: 10px;
66
- }
67
- }
68
-
69
29
  &__section-title {
70
30
  margin: 15px 0 10px;
71
31
 
@@ -73,9 +33,4 @@
73
33
  font-weight: 600;
74
34
  line-height: var(--yc-text-body-2-line-height);
75
35
  }
76
-
77
- &__link {
78
- margin-left: 5px;
79
- @extend .link;
80
- }
81
36
  }
@@ -0,0 +1,52 @@
1
+ import type {TEvDescribeSchemeResult, TIndexDescription} from '../../types/api/schema';
2
+ import {InfoViewer, createInfoFormatter} from '../InfoViewer';
3
+
4
+ const DISPLAYED_FIELDS: Set<keyof TIndexDescription> = new Set([
5
+ 'Type',
6
+ 'State',
7
+ 'DataSize',
8
+ 'KeyColumnNames',
9
+ 'DataColumnNames',
10
+ ]);
11
+
12
+ const formatItem = createInfoFormatter<TIndexDescription>({
13
+ Type: (value) => value?.substring(10), // trims EIndexType prefix
14
+ State: (value) => value?.substring(11), // trims EIndexState prefix
15
+ KeyColumnNames: (value) => value?.join(', '),
16
+ DataColumnNames: (value) => value?.join(', '),
17
+ }, {
18
+ KeyColumnNames: 'Columns',
19
+ DataColumnNames: 'Includes',
20
+ });
21
+
22
+ interface IndexInfoViewerProps {
23
+ data?: TEvDescribeSchemeResult;
24
+ }
25
+
26
+ export const IndexInfoViewer = ({data}: IndexInfoViewerProps) => {
27
+ if (!data) {
28
+ return (
29
+ <div className="error">no index data</div>
30
+ );
31
+ }
32
+
33
+ const TableIndex = data.PathDescription?.TableIndex;
34
+ const info: Array<{label?: string, value?: unknown}> = [];
35
+
36
+ let key: keyof TIndexDescription;
37
+ for (key in TableIndex) {
38
+ if (DISPLAYED_FIELDS.has(key)) {
39
+ info.push(formatItem(key, TableIndex?.[key]));
40
+ }
41
+ }
42
+
43
+ return (
44
+ <>
45
+ {info.length ? (
46
+ <InfoViewer info={info}></InfoViewer>
47
+ ) : (
48
+ <>Empty</>
49
+ )}
50
+ </>
51
+ );
52
+ };
@@ -0,0 +1,4 @@
1
+ import InfoViewer from './InfoViewer';
2
+
3
+ export {InfoViewer};
4
+ export * from './utils';
@@ -0,0 +1,32 @@
1
+ type LabelMap<T> = {
2
+ [label in keyof T]?: string;
3
+ }
4
+
5
+ type FieldMappers<T> = {
6
+ [label in keyof T]?: (value: T[label]) => string | undefined;
7
+ }
8
+
9
+ function formatLabel<Shape>(label: keyof Shape, map: LabelMap<Shape>) {
10
+ return map[label] ?? label;
11
+ }
12
+
13
+ function formatValue<Shape, Key extends keyof Shape>(
14
+ label: Key,
15
+ value: Shape[Key],
16
+ mappers: FieldMappers<Shape>,
17
+ ) {
18
+ const mapper = mappers[label];
19
+ const mappedValue = mapper ? mapper(value) : value;
20
+
21
+ return String(mappedValue ?? '');
22
+ }
23
+
24
+ export function createInfoFormatter<Shape extends Record<string, any>>(
25
+ fieldMappers?: FieldMappers<Shape>,
26
+ labelMap?: LabelMap<Shape>,
27
+ ) {
28
+ return <Key extends keyof Shape>(label: Key, value: Shape[Key]) => ({
29
+ label: formatLabel(label, labelMap || {}),
30
+ value: formatValue(label, value, fieldMappers || {}),
31
+ });
32
+ }
@@ -76,7 +76,7 @@ export class ProgressViewer extends React.Component {
76
76
 
77
77
  if (!isNaN(fillWidth)) {
78
78
  return (
79
- <div className={b({size})}>
79
+ <div className={b({size}, className)}>
80
80
  <div className={b('line', {bg})} style={lineStyle}></div>
81
81
  <span className={b('text', {text})}>
82
82
  {`${valueText} ${divider} ${capacityText}`}
@@ -4,6 +4,10 @@
4
4
  overflow: auto;
5
5
  @include flex-container();
6
6
 
7
+ &__header {
8
+ margin: 16px 20px;
9
+ }
10
+
7
11
  &__content {
8
12
  position: relative;
9
13
 
@@ -19,7 +23,7 @@
19
23
  }
20
24
 
21
25
  &__tabs {
22
- padding: 16px 20px 0;
26
+ padding: 0 20px;
23
27
  }
24
28
 
25
29
  &__tab {
@@ -13,6 +13,7 @@ import Storage from '../Storage/Storage';
13
13
  import NodeOverview from './NodeOverview/NodeOverview';
14
14
  import NodeStructure from './NodeStructure/NodeStructure';
15
15
  import Loader from '../../components/Loader/Loader';
16
+ import {BasicNodeViewer} from '../../components/BasicNodeViewer';
16
17
 
17
18
  import {getNodeInfo, resetNode} from '../../store/reducers/node';
18
19
  import routes, {CLUSTER_PAGES, createHref} from '../../routes';
@@ -133,7 +134,6 @@ function Node(props: NodeProps) {
133
134
  case OVERVIEW: {
134
135
  return (
135
136
  <NodeOverview
136
- additionalNodesInfo={additionalNodesInfo}
137
137
  node={node}
138
138
  className={b('overview-wrapper')}
139
139
  />
@@ -162,6 +162,12 @@ function Node(props: NodeProps) {
162
162
  if (node) {
163
163
  return (
164
164
  <div className={b(null, props.className)}>
165
+ <BasicNodeViewer
166
+ node={node}
167
+ additionalNodesInfo={props.additionalNodesInfo}
168
+ className={b('header')}
169
+ />
170
+
165
171
  {renderTabs()}
166
172
 
167
173
  <div className={b('content')}>{renderTabContent()}</div>
@@ -5,16 +5,14 @@ import {backend} from '../../../store';
5
5
 
6
6
  interface NodeOverviewProps {
7
7
  node: any;
8
- additionalNodesInfo: any;
9
8
  className?: string;
10
9
  }
11
10
 
12
- function NodeOverview({node, additionalNodesInfo, className}: NodeOverviewProps) {
11
+ function NodeOverview({node, className}: NodeOverviewProps) {
13
12
  return (
14
13
  <FullNodeViewer
15
14
  node={node}
16
15
  backend={backend}
17
- additionalNodesInfo={additionalNodesInfo}
18
16
  className={className}
19
17
  />
20
18
  );
@@ -44,9 +44,38 @@
44
44
  &__pdisk-title-wrapper {
45
45
  display: flex;
46
46
  align-items: center;
47
- gap: 8px;
47
+ gap: 16px;
48
48
 
49
49
  font-weight: 600;
50
+
51
+ .entity-status__status-icon {
52
+ margin-right: 0;
53
+ }
54
+ }
55
+
56
+ &__pdisk-title-item {
57
+ display: flex;
58
+ gap: 4px;
59
+
60
+ &-label {
61
+ font-weight: 400;
62
+
63
+ color: var(--yc-color-text-secondary);
64
+ }
65
+ }
66
+
67
+ &__pdisk-title-id {
68
+ min-width: 110px;
69
+ }
70
+
71
+ &__pdisk-title-type {
72
+ justify-content: flex-end;
73
+
74
+ min-width: 50px;
75
+ }
76
+
77
+ &__pdisk-title-size {
78
+ min-width: 150px;
50
79
  }
51
80
 
52
81
  &__pdisk-details {
@@ -0,0 +1,25 @@
1
+ import {ReactNode} from 'react';
2
+ import cn from 'bem-cn-lite';
3
+
4
+ const b = cn('kv-node-structure');
5
+
6
+ interface PDiskTitleBadgeProps {
7
+ label?: string;
8
+ value: ReactNode;
9
+ className?: string;
10
+ }
11
+
12
+ export function PDiskTitleBadge({label, value, className}: PDiskTitleBadgeProps) {
13
+ return (
14
+ <span className={b('pdisk-title-item', className)}>
15
+ {label && (
16
+ <span className={b('pdisk-title-item-label')}>
17
+ {label}:
18
+ </span>
19
+ )}
20
+ <span className={b('pdisk-title-item-value')}>
21
+ {value}
22
+ </span>
23
+ </span>
24
+ );
25
+ }
@@ -14,9 +14,11 @@ import {Vdisk} from './Vdisk';
14
14
 
15
15
  import {bytesToGB, pad9} from '../../../utils/utils';
16
16
  import {formatStorageValuesToGb} from '../../../utils';
17
+ import {getPDiskType} from '../../../utils/pdisk';
17
18
 
18
19
  import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants';
19
20
  import {valueIsDefined} from './NodeStructure';
21
+ import {PDiskTitleBadge} from './PDiskTitleBadge';
20
22
 
21
23
  const b = cn('kv-node-structure');
22
24
 
@@ -230,6 +232,7 @@ export function PDisk(props: PDiskProps) {
230
232
  }
231
233
  if (valueIsDefined(Category)) {
232
234
  pdiskInfo.push({label: 'Category', value: Category});
235
+ pdiskInfo.push({label: 'Type', value: getPDiskType(data)});
233
236
  }
234
237
  pdiskInfo.push({
235
238
  label: 'Allocated Size',
@@ -286,8 +289,27 @@ export function PDisk(props: PDiskProps) {
286
289
  <div className={b('pdisk')} id={props.id}>
287
290
  <div className={b('pdisk-header')}>
288
291
  <div className={b('pdisk-title-wrapper')}>
289
- <span>{data.Path}</span>
290
- <EntityStatus status={data.Device} name={`${data.NodeId}-${data.PDiskId}`} />
292
+ <EntityStatus status={data.Device} />
293
+ <PDiskTitleBadge
294
+ label="PDiskID"
295
+ value={data.PDiskId}
296
+ className={b('pdisk-title-id')}
297
+ />
298
+ <PDiskTitleBadge
299
+ value={getPDiskType(data)}
300
+ className={b('pdisk-title-type')}
301
+ />
302
+ <ProgressViewer
303
+ value={data.TotalSize - data.AvailableSize}
304
+ capacity={data.TotalSize}
305
+ formatValues={formatStorageValuesToGb}
306
+ colorizeProgress={true}
307
+ className={b('pdisk-title-size')}
308
+ />
309
+ <PDiskTitleBadge
310
+ label="VDisks"
311
+ value={data.vDisks.length}
312
+ />
291
313
  </div>
292
314
  <Button onClick={unfolded ? onClosePDiskDetails : onOpenPDiskDetails} view="flat-secondary">
293
315
  <ArrowToggle direction={unfolded ? 'top' : 'bottom'} />
@@ -63,6 +63,7 @@ class Nodes extends React.Component {
63
63
  }
64
64
 
65
65
  componentWillUnmount() {
66
+ this.props.hideTooltip();
66
67
  clearInterval(this.reloadDescriptor);
67
68
  }
68
69
 
@@ -6,9 +6,10 @@ import {Loader} from '@yandex-cloud/uikit';
6
6
 
7
7
  //@ts-ignore
8
8
  import SchemaInfoViewer from '../../Schema/SchemaInfoViewer/SchemaInfoViewer';
9
+ import {IndexInfoViewer} from '../../../../components/IndexInfoViewer/IndexInfoViewer';
9
10
 
10
11
  import type {EPathType} from '../../../../types/api/schema';
11
- import {isColumnEntityType, isTableType} from '../../utils/schema';
12
+ import {isColumnEntityType, isTableType, mapPathTypeToNavigationTreeType} from '../../utils/schema';
12
13
  import {AutoFetcher} from '../../../../utils/autofetcher';
13
14
  //@ts-ignore
14
15
  import {getSchema} from '../../../../store/reducers/schema';
@@ -112,11 +113,24 @@ function Overview(props: OverviewProps) {
112
113
  );
113
114
  };
114
115
 
116
+ const renderContent = () => {
117
+ switch (mapPathTypeToNavigationTreeType(props.type)) {
118
+ case 'index':
119
+ return (
120
+ <IndexInfoViewer data={schemaData} />
121
+ );
122
+ default:
123
+ return (
124
+ <SchemaInfoViewer fullPath={currentItem.Path} data={schemaData} />
125
+ );
126
+ }
127
+ }
128
+
115
129
  return loading && !wasLoaded ? (
116
130
  renderLoader()
117
131
  ) : (
118
132
  <div className={props.className}>
119
- <SchemaInfoViewer fullPath={currentItem.Path} data={schemaData} />
133
+ {renderContent()}
120
134
  </div>
121
135
  );
122
136
  }
@@ -28,29 +28,33 @@ class SchemaInfoViewer extends React.Component {
28
28
  if (data) {
29
29
  const {PathDescription = {}} = data;
30
30
  const {TableStats = {}, TabletMetrics = {}} = PathDescription;
31
- const tableStatsInfo =
32
- TableStats &&
33
- Object.keys(TableStats).map((key) => ({
34
- label: key,
35
- value: TableStats[key].toString(),
36
- }));
31
+ const {PartCount, ...restTableStats} = TableStats;
37
32
 
38
- const tabletMetricsInfo =
39
- TableStats &&
40
- Object.keys(TabletMetrics).map((key) => ({
41
- label: key,
42
- value: this.formatTabletMetricsValue(key, TabletMetrics[key].toString()),
43
- }));
33
+ const priorityInfo = [{
34
+ label: 'PartCount',
35
+ value: PartCount,
36
+ }].filter(({value}) => value !== undefined);
44
37
 
45
- let generalInfo = Object.assign(tableStatsInfo, tabletMetricsInfo);
46
- generalInfo = Object.assign(generalInfo);
38
+ const tableStatsInfo = Object.keys(restTableStats).map((key) => ({
39
+ label: key,
40
+ value: TableStats[key].toString(),
41
+ }));
47
42
 
48
- const infoLength = Object.keys(generalInfo).length;
43
+ const tabletMetricsInfo = Object.keys(TabletMetrics).map((key) => ({
44
+ label: key,
45
+ value: this.formatTabletMetricsValue(key, TabletMetrics[key].toString()),
46
+ }));
49
47
 
48
+ const generalInfo = [
49
+ ...priorityInfo,
50
+ ...tabletMetricsInfo,
51
+ ...tableStatsInfo,
52
+ ];
53
+
50
54
  return (
51
55
  <div className={b()}>
52
56
  <div className={b('item')}>
53
- {infoLength ? (
57
+ {generalInfo.length ? (
54
58
  <InfoViewer info={generalInfo}></InfoViewer>
55
59
  ) : (
56
60
  <div>Empty</div>
@@ -61,6 +61,7 @@ export function SchemaTree(props: SchemaTreeProps) {
61
61
  activePath={currentPath}
62
62
  onActivePathUpdate={handleActivePathUpdate}
63
63
  cache={false}
64
+ virtualize
64
65
  />
65
66
  );
66
67
  }
@@ -54,6 +54,8 @@ interface TPathDescription {
54
54
 
55
55
  ColumnStoreDescription?: unknown;
56
56
  ColumnTableDescription?: unknown;
57
+
58
+ TableIndex?: TIndexDescription;
57
59
  }
58
60
 
59
61
  interface TDirEntry {
@@ -80,6 +82,27 @@ interface TDirEntry {
80
82
  Version?: TPathVersion;
81
83
  }
82
84
 
85
+ export interface TIndexDescription {
86
+ Name?: string;
87
+ /** uint64 */
88
+ LocalPathId?: string;
89
+
90
+ Type?: EIndexType;
91
+ State?: EIndexState;
92
+
93
+ KeyColumnNames?: string[];
94
+
95
+ /** uint64 */
96
+ SchemaVersion?: string;
97
+
98
+ /** uint64 */
99
+ PathOwnerId?: string;
100
+
101
+ DataColumnNames?: string[];
102
+ /** uint64 */
103
+ DataSize?: string;
104
+ }
105
+
83
106
  // incomplete
84
107
  export enum EPathType {
85
108
  EPathTypeInvalid = 'EPathTypeInvalid',
@@ -112,6 +135,19 @@ enum EPathState {
112
135
  EPathStateMoving = 'EPathStateMoving',
113
136
  }
114
137
 
138
+ enum EIndexType {
139
+ EIndexTypeInvalid = 'EIndexTypeInvalid',
140
+ EIndexTypeGlobal = 'EIndexTypeGlobal',
141
+ EIndexTypeGlobalAsync = 'EIndexTypeGlobalAsync',
142
+ }
143
+
144
+ enum EIndexState {
145
+ EIndexStateInvalid = 'EIndexStateInvalid',
146
+ EIndexStateReady = 'EIndexStateReady',
147
+ EIndexStateNotReady = 'EIndexStateNotReady',
148
+ EIndexStateWriteOnly = 'EIndexStateWriteOnly',
149
+ }
150
+
115
151
  // incomplete
116
152
  interface TPathVersion {
117
153
  /** uint64 */
@@ -0,0 +1,54 @@
1
+ enum EFlag {
2
+ Grey = 'Grey',
3
+ Green = 'Green',
4
+ Yellow = 'Yellow',
5
+ Orange = 'Orange',
6
+ Red = 'Red',
7
+ }
8
+
9
+ enum TPDiskState {
10
+ Initial = 'Initial',
11
+ InitialFormatRead = 'InitialFormatRead',
12
+ InitialFormatReadError = 'InitialFormatReadError',
13
+ InitialSysLogRead = 'InitialSysLogRead',
14
+ InitialSysLogReadError = 'InitialSysLogReadError',
15
+ InitialSysLogParseError = 'InitialSysLogParseError',
16
+ InitialCommonLogRead = 'InitialCommonLogRead',
17
+ InitialCommonLogReadError = 'InitialCommonLogReadError',
18
+ InitialCommonLogParseError = 'InitialCommonLogParseError',
19
+ CommonLoggerInitError = 'CommonLoggerInitError',
20
+ Normal = 'Normal',
21
+ OpenFileError = 'OpenFileError',
22
+ ChunkQuotaError = 'ChunkQuotaError',
23
+ DeviceIoError = 'DeviceIoError',
24
+
25
+ Missing = 'Missing',
26
+ Timeout = 'Timeout',
27
+ NodeDisconnected = 'NodeDisconnected',
28
+ Unknown = 'Unknown',
29
+ }
30
+
31
+ export interface TPDiskStateInfo {
32
+ PDiskId?: number;
33
+ /** uint64 */
34
+ CreateTime?: string;
35
+ /** uint64 */
36
+ ChangeTime?: string;
37
+ Path?: string;
38
+ /** uint64 */
39
+ Guid?: string;
40
+ /** uint64 */
41
+ Category?: string;
42
+ /** uint64 */
43
+ AvailableSize?: string;
44
+ /** uint64 */
45
+ TotalSize?: string;
46
+ State?: TPDiskState;
47
+ NodeId?: number;
48
+ Count?: number;
49
+ Device?: EFlag;
50
+ Realtime?: EFlag;
51
+ StateFlag?: EFlag;
52
+ Overall?: EFlag;
53
+ SerialNumber?: string;
54
+ }
@@ -33,6 +33,8 @@ export function getNodesColumns({showTooltip, hideTooltip, tabletsPath, getNodeR
33
33
  <div className={b('host-name-wrapper')}>
34
34
  <EntityStatus
35
35
  name={row.Host}
36
+ onNameMouseEnter={(e) => showTooltip(e.target, row, 'nodeEndpoints')}
37
+ onNameMouseLeave={hideTooltip}
36
38
  status={row.Overall}
37
39
  path={getDefaultNodePath(row.NodeId)}
38
40
  hasClipboardButton
@@ -0,0 +1,74 @@
1
+ import type {TPDiskStateInfo} from "../types/api/storage";
2
+
3
+ // TODO: move to utils or index after converting them to TS
4
+ /**
5
+ * Parses a binary string containing a bit field into an object with binary values.
6
+ * This is an implementation based on string manipulation, since JS can only perform
7
+ * bitwise operations with 32-bits integers, and YDB sends uint64.
8
+ * @see https://en.cppreference.com/w/c/language/bit_field
9
+ * @param binaryString - binary string representing a bit field
10
+ * @param bitFieldStruct - bit field description, <field => size in bits>, in order starting from the rightmost bit
11
+ * @returns object with binary values
12
+ */
13
+ export const parseBitField = <T extends Record<string, number>>(
14
+ binaryString: string,
15
+ bitFieldStruct: T,
16
+ ): Record<keyof T, string> => {
17
+ const fields: Partial<Record<keyof T, string>> = {};
18
+
19
+ Object.entries(bitFieldStruct).reduce((prefixSize, [field, size]: [keyof T, number]) => {
20
+ const end = binaryString.length - prefixSize;
21
+ const start = end - size;
22
+ fields[field] = binaryString.substring(start, end) || '0';
23
+
24
+ return prefixSize + size;
25
+ }, 0);
26
+
27
+ return fields as Record<keyof T, string>;
28
+ };
29
+
30
+ export enum IPDiskType {
31
+ ROT = 'ROT',
32
+ SSD = 'SSD',
33
+ MVME = 'NVME',
34
+ }
35
+
36
+ // Bear with me.
37
+ // Disk type is determined by the field Category.
38
+ // Category is a bit field defined as follows:
39
+ // struct {
40
+ // ui64 IsSolidState : 1;
41
+ // ui64 Kind : 55;
42
+ // ui64 TypeExt : 8;
43
+ // }
44
+ // For compatibility TypeExt is not used for old types (ROT, SSD), so the following scheme is used:
45
+ // ROT -> IsSolidState# 0, TypeExt# 0
46
+ // SSD -> IsSolidState# 1, TypeExt# 0
47
+ // NVME -> IsSolidState# 1, TypeExt# 2
48
+ // Reference on bit fields: https://en.cppreference.com/w/c/language/bit_field
49
+ export const getPDiskType = (data: TPDiskStateInfo): IPDiskType | undefined => {
50
+ if (!data.Category) {
51
+ return undefined;
52
+ }
53
+
54
+ // Category is uint64, too big for Number or bitwise operators, thus BigInt and a custom parser
55
+ const categotyBin = BigInt(data.Category).toString(2);
56
+ const categoryBitField = parseBitField(categotyBin, {
57
+ isSolidState: 1,
58
+ kind: 55,
59
+ typeExt: 8,
60
+ });
61
+
62
+ if (categoryBitField.isSolidState === '1') {
63
+ switch (parseInt(categoryBitField.typeExt, 2)) {
64
+ case 0:
65
+ return IPDiskType.SSD;
66
+ case 2:
67
+ return IPDiskType.MVME;
68
+ }
69
+ } else if (categoryBitField.typeExt === '0') {
70
+ return IPDiskType.ROT;
71
+ }
72
+
73
+ return undefined;
74
+ };
@@ -109,6 +109,32 @@ const NodeTooltip = (props) => {
109
109
  );
110
110
  };
111
111
 
112
+ const NodeEndpointsTooltip = (props) => {
113
+ const {data} = props;
114
+ return (
115
+ data && (
116
+ <div className={nodeB()}>
117
+ <table>
118
+ <tbody>
119
+ {data.Rack && (
120
+ <tr>
121
+ <td className={nodeB('label')}>Rack</td>
122
+ <td className={nodeB('value')}>{data.Rack}</td>
123
+ </tr>
124
+ )}
125
+ {data.Endpoints && data.Endpoints.length && data.Endpoints.map(({Name, Address}) => (
126
+ <tr key={Name}>
127
+ <td className={nodeB('label')}>{Name}</td>
128
+ <td className={nodeB('value')}>{Address}</td>
129
+ </tr>
130
+ ))}
131
+ </tbody>
132
+ </table>
133
+ </div>
134
+ )
135
+ );
136
+ };
137
+
112
138
  const tabletsOverallB = cn('tabletsOverall-tooltip');
113
139
 
114
140
  const TabletsOverallTooltip = (props) => {
@@ -175,6 +201,7 @@ export const tooltipTemplates = {
175
201
  tablet: (data, additionalData) => <TabletTooltip data={data} additionalData={additionalData} />,
176
202
  // eslint-disable-next-line react/display-name
177
203
  node: (data) => <NodeTooltip data={data} />,
204
+ nodeEndpoints: (data) => <NodeEndpointsTooltip data={data} />,
178
205
  // eslint-disable-next-line react/display-name
179
206
  tabletsOverall: (data) => <TabletsOverallTooltip data={data} />,
180
207
  // eslint-disable-next-line react/display-name
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "1.8.8",
3
+ "version": "1.9.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -40,7 +40,7 @@
40
40
  "reselect": "4.0.0",
41
41
  "sass": "1.32.8",
42
42
  "web-vitals": "1.1.2",
43
- "ydb-ui-components": "2.3.0"
43
+ "ydb-ui-components": "2.4.0"
44
44
  },
45
45
  "scripts": {
46
46
  "start": "react-app-rewired start",