ydb-embedded-ui 1.8.6 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. package/CHANGELOG.md +41 -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/components/TabletsOverall/TabletsOverall.tsx +1 -1
  13. package/dist/containers/App/App.js +3 -5
  14. package/dist/containers/Node/Node.scss +5 -1
  15. package/dist/containers/Node/Node.tsx +7 -1
  16. package/dist/containers/Node/NodeOverview/NodeOverview.tsx +1 -3
  17. package/dist/containers/Node/NodeStructure/NodeStructure.scss +30 -1
  18. package/dist/containers/Node/NodeStructure/PDiskTitleBadge.tsx +25 -0
  19. package/dist/containers/Node/NodeStructure/Pdisk.tsx +24 -2
  20. package/dist/containers/Nodes/Nodes.js +1 -0
  21. package/dist/containers/ReduxTooltip/ReduxTooltip.js +1 -1
  22. package/dist/containers/Tablets/Tablets.js +1 -1
  23. package/dist/containers/Tablets/Tablets.scss +0 -6
  24. package/dist/containers/TabletsFilters/TabletsFilters.scss +0 -7
  25. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.scss +6 -2
  26. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.js +6 -2
  27. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +6 -2
  28. package/dist/containers/Tenant/Diagnostics/Overview/Overview.tsx +16 -2
  29. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.js +15 -13
  30. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.scss +22 -6
  31. package/dist/containers/Tenant/Preview/Preview.js +3 -0
  32. package/dist/containers/Tenant/Schema/SchemaInfoViewer/SchemaInfoViewer.js +20 -16
  33. package/dist/containers/Tenant/Schema/SchemaTree/SchemaTree.tsx +1 -0
  34. package/dist/types/api/schema.ts +36 -0
  35. package/dist/types/api/storage.ts +54 -0
  36. package/dist/utils/getNodesColumns.js +2 -0
  37. package/dist/utils/pdisk.ts +74 -0
  38. package/dist/utils/tooltip.js +27 -0
  39. package/dist/utils/utils.js +8 -1
  40. package/package.json +2 -2
@@ -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
 
@@ -11,7 +11,7 @@ const propTypes = {
11
11
  className: PropTypes.string,
12
12
  toolTipVisible: PropTypes.bool,
13
13
  currentHoveredRef: PropTypes.object,
14
- data: PropTypes.object,
14
+ data: PropTypes.any,
15
15
  template: PropTypes.func,
16
16
  hideTooltip: PropTypes.func,
17
17
  };
@@ -150,7 +150,7 @@ class Tablets extends React.Component {
150
150
  const {stateFilter, typeFilter, className} = this.props;
151
151
 
152
152
  return (
153
- <div className={(b(), className)}>
153
+ <div className={b(null, className)}>
154
154
  <div className={b('header')}>
155
155
  <Select
156
156
  className={b('filter-control')}
@@ -13,13 +13,7 @@
13
13
  }
14
14
 
15
15
  &__items {
16
- display: flex;
17
16
  flex: 1 1 auto;
18
- flex-wrap: wrap;
19
- }
20
-
21
- &__items-wrapper {
22
- overflow: auto;
23
17
  }
24
18
 
25
19
  &__filters {
@@ -3,7 +3,6 @@
3
3
  .tablets-filters {
4
4
  overflow: auto;
5
5
 
6
- max-height: 400px;
7
6
  @include flex-container();
8
7
 
9
8
  &__node {
@@ -19,18 +18,12 @@
19
18
  }
20
19
 
21
20
  &__items {
22
- display: flex;
23
21
  overflow: auto;
24
22
  flex: 1 1 auto;
25
- flex-wrap: wrap;
26
23
 
27
24
  padding: 5px 20px;
28
25
  }
29
26
 
30
- &__items-wrapper {
31
- overflow: auto;
32
- }
33
-
34
27
  &__filters {
35
28
  display: flex;
36
29
  align-items: center;
@@ -1,9 +1,13 @@
1
+ $section-title-margin: 20px;
2
+ $section-title-line-height: 24px;
3
+
1
4
  .kv-detailed-overview {
2
5
  display: flex;
3
- gap: 10px;
6
+ gap: 20px;
4
7
  &__section {
5
8
  display: flex;
6
- flex: 0 0 50%;
9
+ overflow-x: hidden;
10
+ flex: 0 0 calc(50% - 10px);
7
11
  flex-direction: column;
8
12
  }
9
13
 
@@ -97,10 +97,14 @@ class Healthcheck extends React.Component {
97
97
  </div>
98
98
  {this.renderUpdateButton()}
99
99
  </div>
100
- <div>
100
+ <div className={b('preview-content')}>
101
101
  {text}
102
102
  {!statusOk && (
103
- <Button view="flat-info" onClick={showMoreHandler}>
103
+ <Button
104
+ view="flat-info"
105
+ onClick={showMoreHandler}
106
+ size="s"
107
+ >
104
108
  Show details
105
109
  </Button>
106
110
  )}
@@ -1,3 +1,4 @@
1
+ @use '../DetailedOverview/DetailedOverview.scss' as detailedOverview;
1
2
  @import '../../../../styles/mixins.scss';
2
3
 
3
4
  .healthcheck {
@@ -37,14 +38,17 @@
37
38
 
38
39
  &__status-wrapper {
39
40
  display: flex;
40
- align-items: baseline;
41
41
 
42
- margin-bottom: 15px;
42
+ margin-bottom: detailedOverview.$section-title-margin;
43
43
  gap: 8px;
44
44
  }
45
45
 
46
46
  &__preview-title {
47
47
  font-weight: 600;
48
+ line-height: detailedOverview.$section-title-line-height;
49
+ }
50
+
51
+ &__preview-content {
48
52
  line-height: 24px;
49
53
  }
50
54
 
@@ -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
  }
@@ -25,9 +25,11 @@ const renderName = (tenant) => {
25
25
  if (tenant) {
26
26
  const {Name} = tenant;
27
27
  return (
28
- <div className={b('tenant-name')}>
28
+ <div className={b('tenant-name-wrapper')}>
29
29
  <EntityStatus status={tenant.State} />
30
- <span>{Name}</span>
30
+ <span className={b('tenant-name-trim')}>
31
+ <span className={b('tenant-name')}>{Name}</span>
32
+ </span>
31
33
  </div>
32
34
  );
33
35
  }
@@ -139,17 +141,17 @@ class TenantOverview extends React.Component {
139
141
  this.props.tenant.Name,
140
142
  this.props.tenant.Type,
141
143
  )}
142
- <div className={b('system-tablets')}>
143
- {SystemTablets &&
144
- SystemTablets.map((tablet, tabletIndex) => (
145
- <Tablet
146
- onMouseEnter={showTooltip}
147
- onMouseLeave={hideTooltip}
148
- key={tabletIndex}
149
- tablet={tablet}
150
- />
151
- ))}
152
- </div>
144
+ </div>
145
+ <div className={b('system-tablets')}>
146
+ {SystemTablets &&
147
+ SystemTablets.map((tablet, tabletIndex) => (
148
+ <Tablet
149
+ onMouseEnter={showTooltip}
150
+ onMouseLeave={hideTooltip}
151
+ key={tabletIndex}
152
+ tablet={tablet}
153
+ />
154
+ ))}
153
155
  </div>
154
156
  <div className={b('common-info')}>
155
157
  {PoolStats ? (
@@ -1,30 +1,46 @@
1
+ @use '../DetailedOverview/DetailedOverview.scss' as detailedOverview;
2
+
1
3
  .tenant-overview {
2
4
  padding-bottom: 20px;
3
5
  &__loader {
4
6
  display: flex;
5
7
  justify-content: center;
6
8
  }
7
- &__tenant-name {
9
+ &__tenant-name-wrapper {
8
10
  display: flex;
11
+ overflow: hidden;
9
12
  align-items: center;
10
13
 
11
14
  & .yc-link {
12
15
  display: flex;
13
16
  }
14
17
  }
18
+ &__tenant-name-trim {
19
+ overflow: hidden;
20
+
21
+ white-space: nowrap;
22
+ text-overflow: ellipsis;
23
+ direction: rtl;
24
+ }
25
+
26
+ &__tenant-name {
27
+ unicode-bidi: plaintext;
28
+ }
15
29
 
16
30
  &__top {
17
31
  display: flex;
18
32
  align-items: center;
19
33
 
20
- margin-bottom: 20px;
34
+ margin-bottom: 10px;
35
+
36
+ line-height: 24px;
21
37
  }
22
38
 
23
39
  &__top-label {
24
- margin-bottom: 20px;
40
+ margin-bottom: detailedOverview.$section-title-margin;
25
41
 
26
42
  font-weight: 600;
27
- line-height: 24px;
43
+ line-height: detailedOverview.$section-title-line-height;
28
44
  gap: 10px;
29
45
  }
30
46
 
@@ -42,7 +58,7 @@
42
58
  flex-wrap: wrap;
43
59
  align-items: center;
44
60
 
45
- margin-left: 15px;
61
+ margin-bottom: 35px;
46
62
  }
47
63
 
48
64
  &__collapse-title {
@@ -76,7 +92,7 @@
76
92
  margin-bottom: 20px;
77
93
 
78
94
  font-size: var(--yc-text-body-2-font-size);
79
- font-weight: 500;
95
+ font-weight: 600;
80
96
  line-height: var(--yc-text-body-2-line-height);
81
97
  }
82
98
  }
@@ -12,6 +12,7 @@ import Fullscreen from '../../../components/Fullscreen/Fullscreen';
12
12
  import {sendQuery, setQueryOptions} from '../../../store/reducers/preview';
13
13
  import {showTooltip, hideTooltip} from '../../../store/reducers/tooltip';
14
14
  import {prepareQueryError, prepareQueryResponse} from '../../../utils/index';
15
+ import {isNumeric} from '../../../utils/utils';
15
16
 
16
17
  import {isTableType} from '../utils/schema';
17
18
  import {AutoFetcher} from '../../../utils/autofetcher';
@@ -127,6 +128,8 @@ class Preview extends React.Component {
127
128
  if (data && data.length > 0) {
128
129
  columns = Object.keys(data[0]).map((key) => ({
129
130
  name: key,
131
+ align: isNumeric(data[0][key]) ? DataTable.RIGHT : DataTable.LEFT,
132
+ sortAccessor: (row) => isNumeric(row[key]) ? Number(row[key]) : row[key],
130
133
  render: ({value}) => {
131
134
  return (
132
135
  <span
@@ -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
@@ -81,4 +81,11 @@ export function pad9(val) {
81
81
  result = "0" + result;
82
82
  }
83
83
  return result;
84
- }
84
+ }
85
+
86
+ export function isNumeric(value) {
87
+ // need both isNaN and isNaN(parseFloat):
88
+ // - isNaN treats true/false/''/etc. as numbers, parseFloat fixes this
89
+ // - parseFloat treats '123qwe' as number, isNaN fixes this
90
+ return !isNaN(value) && !isNaN(parseFloat(value));
91
+ };