ydb-embedded-ui 3.4.3 → 3.4.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/components/Icon/Icon.tsx +6 -0
  3. package/dist/components/InfoViewer/InfoViewer.tsx +2 -2
  4. package/dist/components/InfoViewer/formatters/index.ts +0 -1
  5. package/dist/components/LabelWithPopover/LabelWithPopover.tsx +3 -7
  6. package/dist/components/LagPopoverContent/LagPopoverContent.scss +13 -0
  7. package/dist/components/LagPopoverContent/LagPopoverContent.tsx +19 -0
  8. package/dist/components/LagPopoverContent/index.ts +1 -0
  9. package/dist/components/Tooltips/NodeEndpointsTooltip/NodeEndpointsTooltip.scss +5 -0
  10. package/dist/components/Tooltips/NodeEndpointsTooltip/NodeEndpointsTooltip.tsx +31 -0
  11. package/dist/containers/Nodes/Nodes.tsx +17 -15
  12. package/dist/containers/Nodes/NodesTable.scss +11 -10
  13. package/dist/containers/Nodes/getNodesColumns.tsx +29 -24
  14. package/dist/containers/Storage/PDisk/PDisk.scss +2 -0
  15. package/dist/containers/Storage/PDiskPopup/PDiskPopup.tsx +6 -9
  16. package/dist/containers/Storage/Storage.js +12 -5
  17. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +3 -1
  18. package/dist/containers/Storage/StorageNodes/StorageNodes.scss +20 -7
  19. package/dist/containers/Storage/StorageNodes/StorageNodes.tsx +43 -7
  20. package/dist/containers/Storage/VDisk/VDisk.tsx +3 -2
  21. package/dist/containers/Storage/VDiskPopup/VDiskPopup.tsx +4 -2
  22. package/dist/containers/Tenant/Diagnostics/Consumers/Headers/Headers.scss +0 -8
  23. package/dist/containers/Tenant/Diagnostics/Consumers/Headers/Headers.tsx +3 -10
  24. package/dist/containers/Tenant/Diagnostics/Consumers/i18n/ru.json +1 -1
  25. package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +2 -1
  26. package/dist/containers/Tenant/Diagnostics/DiagnosticsPages.ts +4 -4
  27. package/dist/containers/Tenant/Diagnostics/Overview/TopicStats/TopicStats.tsx +51 -32
  28. package/dist/containers/Tenant/Diagnostics/Overview/TopicStats/i18n/en.json +2 -1
  29. package/dist/containers/Tenant/Diagnostics/Overview/TopicStats/i18n/ru.json +2 -1
  30. package/dist/containers/Tenant/Diagnostics/Partitions/Headers/Headers.scss +0 -8
  31. package/dist/containers/Tenant/Diagnostics/Partitions/Headers/Headers.tsx +7 -21
  32. package/dist/services/api.d.ts +4 -0
  33. package/dist/store/reducers/{nodesList.js → nodesList.ts} +19 -7
  34. package/dist/store/reducers/storage.js +3 -11
  35. package/dist/types/api/nodesList.ts +25 -0
  36. package/dist/types/store/nodesList.ts +24 -0
  37. package/dist/utils/index.js +9 -1
  38. package/dist/utils/nodes.ts +4 -0
  39. package/dist/utils/tooltip.js +2 -28
  40. package/package.json +1 -1
  41. package/dist/components/InfoViewer/formatters/topicStats.tsx +0 -29
package/CHANGELOG.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.4.5](https://github.com/ydb-platform/ydb-embedded-ui/compare/v3.4.4...v3.4.5) (2023-03-30)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * **Consumers:** fix typo ([aaa9dbd](https://github.com/ydb-platform/ydb-embedded-ui/commit/aaa9dbda1f28702917793a61bae2813f6ef018bb))
9
+ * **PDisk:** add display block to content ([130dab2](https://github.com/ydb-platform/ydb-embedded-ui/commit/130dab20ffdc9da77225c94a6e6064f0308a1c2a))
10
+ * **Storage:** get nodes hosts from /nodelist ([cc82dd9](https://github.com/ydb-platform/ydb-embedded-ui/commit/cc82dd93808133b0d1dcd21b31ee3744df4f7383))
11
+ * **StorageNodes:** make fqdn similar to nodes page ([344298a](https://github.com/ydb-platform/ydb-embedded-ui/commit/344298a9a29380f1068b002fa304cdcc221ce0d4))
12
+ * **TopicInfo:** do not display /s when speed is undefined ([2d41832](https://github.com/ydb-platform/ydb-embedded-ui/commit/2d4183247ec33acdfa45be72a93f0dbd93b716e0))
13
+ * **TopicStats:** use prepared stats, update fields ([a614a8c](https://github.com/ydb-platform/ydb-embedded-ui/commit/a614a8caa2744b844d97f23f25e5385387367d6b))
14
+
15
+ ## [3.4.4](https://github.com/ydb-platform/ydb-embedded-ui/compare/v3.4.3...v3.4.4) (2023-03-22)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **Diagnostics:** display nodes tab for not db entities ([a542dbc](https://github.com/ydb-platform/ydb-embedded-ui/commit/a542dbc23d01138a5c1a4126cfc1836a1543b68c))
21
+
3
22
  ## [3.4.3](https://github.com/ydb-platform/ydb-embedded-ui/compare/v3.4.2...v3.4.3) (2023-03-17)
4
23
 
5
24
 
@@ -31,3 +31,9 @@ export const Icon = ({
31
31
  />
32
32
  );
33
33
  };
34
+
35
+ // When used with uikit components Icon is considered as text element and corresponding styles are applied
36
+ // IconWrapper overrides displayName to 'Icon', so it will be considered as an icon with right styles
37
+ export const IconWrapper = (props: IconProps) => <Icon {...props} />;
38
+
39
+ IconWrapper.displayName = 'Icon';
@@ -4,7 +4,7 @@ import cn from 'bem-cn-lite';
4
4
  import './InfoViewer.scss';
5
5
 
6
6
  export interface InfoViewerItem {
7
- label: string;
7
+ label: ReactNode;
8
8
  value: ReactNode;
9
9
  }
10
10
 
@@ -39,7 +39,7 @@ const InfoViewer = ({
39
39
  {info && info.length > 0 ? (
40
40
  <div className={b('items')}>
41
41
  {info.map((data, infoIndex) => (
42
- <div className={b('row')} key={data.label + infoIndex}>
42
+ <div className={b('row')} key={infoIndex}>
43
43
  <div className={b('label')}>
44
44
  <div className={b('label-text', {multiline: multilineLabels})}>
45
45
  {data.label}
@@ -1,6 +1,5 @@
1
1
  export * from './common';
2
2
  export * from './schema';
3
- export * from './topicStats';
4
3
  export * from './pqGroup';
5
4
  export * from './cdcStream';
6
5
  export * from './table';
@@ -3,18 +3,14 @@ import type {ReactNode} from 'react';
3
3
  import {HelpPopover} from '@gravity-ui/uikit';
4
4
 
5
5
  interface LabelWithPopoverProps {
6
- headerText: string;
6
+ text: string;
7
7
  popoverContent: ReactNode;
8
8
  className?: string;
9
9
  }
10
10
 
11
- export const LabelWithPopover = ({
12
- headerText,
13
- popoverContent,
14
- className,
15
- }: LabelWithPopoverProps) => (
11
+ export const LabelWithPopover = ({text, popoverContent, className}: LabelWithPopoverProps) => (
16
12
  <div className={className}>
17
- {headerText}
13
+ {text}
18
14
  <HelpPopover content={popoverContent} />
19
15
  </div>
20
16
  );
@@ -0,0 +1,13 @@
1
+ .ydb-lag-popover-content {
2
+ &__text {
3
+ margin-bottom: 10px;
4
+ }
5
+
6
+ &_type_read {
7
+ max-width: 280px;
8
+ }
9
+
10
+ &_type_write {
11
+ max-width: 220px;
12
+ }
13
+ }
@@ -0,0 +1,19 @@
1
+ import block from 'bem-cn-lite';
2
+
3
+ import {ReadLagImage, WriteLagImage} from '../LagImages';
4
+
5
+ import './LagPopoverContent.scss';
6
+
7
+ const b = block('ydb-lag-popover-content');
8
+
9
+ interface LagPopoverContentProps {
10
+ text: string;
11
+ type: 'read' | 'write';
12
+ }
13
+
14
+ export const LagPopoverContent = ({text, type}: LagPopoverContentProps) => (
15
+ <div className={b({type})}>
16
+ <div className={b('text')}>{text}</div>
17
+ <div>{type === 'read' ? <ReadLagImage /> : <WriteLagImage />}</div>
18
+ </div>
19
+ );
@@ -0,0 +1 @@
1
+ export * from './LagPopoverContent';
@@ -0,0 +1,5 @@
1
+ .ydb-node-endpoints-tooltip {
2
+ .info-viewer__value {
3
+ min-width: 70px;
4
+ }
5
+ }
@@ -0,0 +1,31 @@
1
+ import block from 'bem-cn-lite';
2
+
3
+ import type {TSystemStateInfo} from '../../../types/api/nodes';
4
+
5
+ import {InfoViewer, InfoViewerItem} from '../../InfoViewer';
6
+
7
+ import './NodeEndpointsTooltip.scss';
8
+
9
+ const b = block('ydb-node-endpoints-tooltip');
10
+
11
+ interface NodeEdpointsTooltipProps {
12
+ data?: TSystemStateInfo;
13
+ }
14
+
15
+ export const NodeEndpointsTooltip = ({data}: NodeEdpointsTooltipProps) => {
16
+ const info: InfoViewerItem[] = [];
17
+
18
+ if (data?.Rack) {
19
+ info.push({label: 'Rack', value: data.Rack});
20
+ }
21
+
22
+ if (data?.Endpoints && data.Endpoints.length) {
23
+ data.Endpoints.forEach(({Name, Address}) => {
24
+ if (Name && Address) {
25
+ info.push({label: Name, value: Address});
26
+ }
27
+ });
28
+ }
29
+
30
+ return <InfoViewer className={b(null)} info={info} dots={false} size={'s'} />;
31
+ };
@@ -4,10 +4,11 @@ import {useDispatch} from 'react-redux';
4
4
 
5
5
  import DataTable from '@gravity-ui/react-data-table';
6
6
 
7
+ import type {EPathType} from '../../types/api/schema';
8
+
7
9
  import {AccessDenied} from '../../components/Errors/403';
8
10
  import {Illustration} from '../../components/Illustration';
9
11
  import {Loader} from '../../components/Loader';
10
-
11
12
  import {Search} from '../../components/Search';
12
13
  import {ProblemFilter} from '../../components/ProblemFilter';
13
14
  import {UptimeFilter} from '../../components/UptimeFIlter';
@@ -21,7 +22,7 @@ import {
21
22
  USE_NODES_ENDPOINT_IN_DIAGNOSTICS_KEY,
22
23
  } from '../../utils/constants';
23
24
  import {useAutofetcher, useTypedSelector} from '../../utils/hooks';
24
- import {isUnavailableNode, NodesUptimeFilterValues} from '../../utils/nodes';
25
+ import {AdditionalNodesInfo, isUnavailableNode, NodesUptimeFilterValues} from '../../utils/nodes';
25
26
 
26
27
  import {setHeader} from '../../store/reducers/header';
27
28
  import {
@@ -35,6 +36,8 @@ import {
35
36
  import {changeFilter, getSettingValue} from '../../store/reducers/settings';
36
37
  import {hideTooltip, showTooltip} from '../../store/reducers/tooltip';
37
38
 
39
+ import {isDatabaseEntityType} from '../Tenant/utils/schema';
40
+
38
41
  import {getNodesColumns} from './getNodesColumns';
39
42
 
40
43
  import './Nodes.scss';
@@ -43,27 +46,24 @@ import i18n from './i18n';
43
46
 
44
47
  const b = cn('ydb-nodes');
45
48
 
46
- interface IAdditionalNodesInfo extends Record<string, unknown> {
47
- getNodeRef?: Function;
48
- }
49
-
50
49
  interface NodesProps {
51
- tenantPath?: string;
50
+ path?: string;
51
+ type?: EPathType;
52
52
  className?: string;
53
- additionalNodesInfo?: IAdditionalNodesInfo;
53
+ additionalNodesInfo?: AdditionalNodesInfo;
54
54
  }
55
55
 
56
- export const Nodes = ({tenantPath, className, additionalNodesInfo = {}}: NodesProps) => {
56
+ export const Nodes = ({path, type, className, additionalNodesInfo = {}}: NodesProps) => {
57
57
  const dispatch = useDispatch();
58
58
 
59
- const isClusterNodes = !tenantPath;
59
+ const isClusterNodes = !path;
60
60
 
61
61
  // Since Nodes component is used in several places,
62
62
  // we need to reset filters, searchValue and loading state
63
63
  // in nodes reducer when path changes
64
64
  useEffect(() => {
65
65
  dispatch(resetNodesState());
66
- }, [dispatch, tenantPath]);
66
+ }, [dispatch, path]);
67
67
 
68
68
  const {wasLoaded, loading, error, nodesUptimeFilter, searchValue, totalNodes} =
69
69
  useTypedSelector((state) => state.nodes);
@@ -77,12 +77,14 @@ export const Nodes = ({tenantPath, className, additionalNodesInfo = {}}: NodesPr
77
77
  );
78
78
 
79
79
  const fetchNodes = useCallback(() => {
80
- if (tenantPath && !JSON.parse(useNodesEndpoint)) {
81
- dispatch(getComputeNodes(tenantPath));
80
+ // For not DB entities we always use /compute endpoint instead of /nodes
81
+ // since /nodes can return data only for tenants
82
+ if (path && (!JSON.parse(useNodesEndpoint) || !isDatabaseEntityType(type))) {
83
+ dispatch(getComputeNodes(path));
82
84
  } else {
83
- dispatch(getNodes({tenant: tenantPath}));
85
+ dispatch(getNodes({tenant: path}));
84
86
  }
85
- }, [dispatch, tenantPath, useNodesEndpoint]);
87
+ }, [dispatch, path, type, useNodesEndpoint]);
86
88
 
87
89
  useAutofetcher(fetchNodes, [fetchNodes], isClusterNodes ? true : autorefresh);
88
90
 
@@ -1,22 +1,23 @@
1
1
  .ydb-nodes-table {
2
- &__host-name-wrapper {
2
+ &__host-field-wrapper {
3
3
  display: flex;
4
4
  }
5
5
 
6
- &__external-button {
7
- display: none;
8
- align-items: center;
9
-
10
- margin-left: 4px;
6
+ &__host-wrapper {
7
+ display: flex;
11
8
 
12
- .yc-button__text {
13
- margin: 0 4px;
14
- }
9
+ max-width: 330px;
15
10
  }
16
11
 
17
- &__host-name {
12
+ &__host {
18
13
  overflow: hidden;
19
14
  }
15
+
16
+ &__external-button {
17
+ display: none;
18
+
19
+ margin-left: 4px;
20
+ }
20
21
  }
21
22
 
22
23
  .data-table__row:hover {
@@ -1,12 +1,13 @@
1
1
  import cn from 'bem-cn-lite';
2
2
  import DataTable, {Column} from '@gravity-ui/react-data-table';
3
- import {Button, Popover} from '@gravity-ui/uikit';
3
+ import {Button, Popover, PopoverBehavior} from '@gravity-ui/uikit';
4
4
 
5
- import {Icon} from '../../components/Icon';
5
+ import {IconWrapper} from '../../components/Icon';
6
6
  import EntityStatus from '../../components/EntityStatus/EntityStatus';
7
7
  import PoolsGraph from '../../components/PoolsGraph/PoolsGraph';
8
8
  import ProgressViewer from '../../components/ProgressViewer/ProgressViewer';
9
9
  import {TabletsStatistic} from '../../components/TabletsStatistic';
10
+ import {NodeEndpointsTooltip} from '../../components/Tooltips/NodeEndpointsTooltip/NodeEndpointsTooltip';
10
11
 
11
12
  import {formatBytesToGigabyte} from '../../utils/index';
12
13
  import {INodesPreparedEntity} from '../../types/store/nodes';
@@ -46,28 +47,32 @@ export function getNodesColumns({
46
47
  return <span>—</span>;
47
48
  }
48
49
  return (
49
- <div className={b('host-name-wrapper')}>
50
- <EntityStatus
51
- name={row.Host}
52
- onNameMouseEnter={(e: MouseEvent) =>
53
- showTooltip(e.target, row, 'nodeEndpoints')
54
- }
55
- onNameMouseLeave={hideTooltip}
56
- status={row.SystemState}
57
- path={getDefaultNodePath(row.NodeId)}
58
- hasClipboardButton
59
- className={b('host-name')}
60
- />
61
- {nodeRef && (
62
- <Button
63
- size="s"
64
- href={nodeRef}
65
- className={b('external-button')}
66
- target="_blank"
67
- >
68
- <Icon name="external" />
69
- </Button>
70
- )}
50
+ <div className={b('host-field-wrapper')}>
51
+ <Popover
52
+ content={<NodeEndpointsTooltip data={row} />}
53
+ placement={['top', 'bottom']}
54
+ behavior={PopoverBehavior.Immediate}
55
+ >
56
+ <div className={b('host-wrapper')}>
57
+ <EntityStatus
58
+ name={row.Host}
59
+ status={row.SystemState}
60
+ path={getDefaultNodePath(row.NodeId)}
61
+ hasClipboardButton
62
+ className={b('host')}
63
+ />
64
+ {nodeRef && (
65
+ <Button
66
+ size="s"
67
+ href={nodeRef}
68
+ className={b('external-button')}
69
+ target="_blank"
70
+ >
71
+ <IconWrapper name="external" />
72
+ </Button>
73
+ )}
74
+ </div>
75
+ </Popover>
71
76
  </div>
72
77
  );
73
78
  },
@@ -6,6 +6,8 @@
6
6
  &__content {
7
7
  position: relative;
8
8
 
9
+ display: block;
10
+
9
11
  border-radius: 4px; // to match interactive area with disk shape
10
12
  }
11
13
 
@@ -3,6 +3,8 @@ import cn from 'bem-cn-lite';
3
3
 
4
4
  import {Popup, PopupProps} from '@gravity-ui/uikit';
5
5
 
6
+ import type {NodesMap} from '../../../types/store/nodesList';
7
+
6
8
  import {InfoViewer, InfoViewerItem} from '../../../components/InfoViewer';
7
9
 
8
10
  import {EFlag} from '../../../types/api/enums';
@@ -17,12 +19,7 @@ const b = cn('pdisk-storage-popup');
17
19
 
18
20
  const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow];
19
21
 
20
- export type NodesHosts = {
21
- // NodeId => Host
22
- [nodeId: number]: string;
23
- }
24
-
25
- export const preparePDiskData = (data: TPDiskStateInfo, nodes?: NodesHosts) => {
22
+ export const preparePDiskData = (data: TPDiskStateInfo, nodes?: NodesMap) => {
26
23
  const {AvailableSize, TotalSize, State, PDiskId, NodeId, Path, Realtime, Device} = data;
27
24
 
28
25
  const pdiskData: InfoViewerItem[] = [
@@ -35,8 +32,8 @@ export const preparePDiskData = (data: TPDiskStateInfo, nodes?: NodesHosts) => {
35
32
  pdiskData.push({label: 'Node Id', value: NodeId});
36
33
  }
37
34
 
38
- if (nodes && NodeId && nodes[NodeId]) {
39
- pdiskData.push({label: 'Host', value: nodes[NodeId]});
35
+ if (nodes && NodeId && nodes.get(NodeId)) {
36
+ pdiskData.push({label: 'Host', value: nodes.get(NodeId)});
40
37
  }
41
38
 
42
39
  if (Path) {
@@ -61,7 +58,7 @@ export const preparePDiskData = (data: TPDiskStateInfo, nodes?: NodesHosts) => {
61
58
 
62
59
  interface PDiskPopupProps extends PopupProps {
63
60
  data: TPDiskStateInfo;
64
- nodes?: NodesHosts;
61
+ nodes?: NodesMap;
65
62
  }
66
63
 
67
64
  export const PDiskPopup = ({data, nodes, ...props}: PDiskPopupProps) => {
@@ -22,7 +22,6 @@ import {
22
22
  setVisibleEntities,
23
23
  setStorageFilter,
24
24
  setUsageFilter,
25
- getNodesObject,
26
25
  StorageTypes,
27
26
  setStorageType,
28
27
  setNodesUptimeFilter,
@@ -32,7 +31,7 @@ import {
32
31
  getStorageNodesCount,
33
32
  getUsageFilterOptions,
34
33
  } from '../../store/reducers/storage';
35
- import {getNodesList} from '../../store/reducers/nodesList';
34
+ import {getNodesList, selectNodesMap} from '../../store/reducers/nodesList';
36
35
  import StorageGroups from './StorageGroups/StorageGroups';
37
36
  import StorageNodes from './StorageNodes/StorageNodes';
38
37
  import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants';
@@ -74,6 +73,7 @@ class Storage extends React.Component {
74
73
  nodesUptimeFilter: PropTypes.string,
75
74
  setNodesUptimeFilter: PropTypes.func,
76
75
  setDataWasNotLoaded: PropTypes.func,
76
+ additionalNodesInfo: PropTypes.object,
77
77
  };
78
78
 
79
79
  componentDidMount() {
@@ -181,8 +181,14 @@ class Storage extends React.Component {
181
181
  }
182
182
 
183
183
  renderDataTable() {
184
- const {flatListStorageEntities, visibleEntities, nodesUptimeFilter, nodes, storageType} =
185
- this.props;
184
+ const {
185
+ flatListStorageEntities,
186
+ visibleEntities,
187
+ nodesUptimeFilter,
188
+ nodes,
189
+ storageType,
190
+ additionalNodesInfo,
191
+ } = this.props;
186
192
 
187
193
  return (
188
194
  <div className={b('table-wrapper')}>
@@ -202,6 +208,7 @@ class Storage extends React.Component {
202
208
  data={flatListStorageEntities}
203
209
  tableSettings={tableSettings}
204
210
  onShowAll={this.onShowAllNodes}
211
+ additionalNodesInfo={additionalNodesInfo}
205
212
  />
206
213
  )}
207
214
  </div>
@@ -345,7 +352,7 @@ function mapStateToProps(state) {
345
352
  flatListStorageEntities: getFilteredEntities(state),
346
353
  groupsCount: getStoragePoolsGroupsCount(state),
347
354
  autorefresh: state.schema.autorefresh,
348
- nodes: getNodesObject(state),
355
+ nodes: selectNodesMap(state),
349
356
  nodesCount: getStorageNodesCount(state),
350
357
  loading,
351
358
  wasLoaded,
@@ -3,6 +3,8 @@ import cn from 'bem-cn-lite';
3
3
  import DataTable, {Column, Settings, SortOrder} from '@gravity-ui/react-data-table';
4
4
  import {Icon, Label, Popover, PopoverBehavior} from '@gravity-ui/uikit';
5
5
 
6
+ import type {NodesMap} from '../../../types/store/nodesList';
7
+
6
8
  import shieldIcon from '../../../assets/icons/shield.svg';
7
9
 
8
10
  import {Stack} from '../../../components/Stack/Stack';
@@ -45,7 +47,7 @@ type TableColumnsIdsValues = typeof TableColumnsIds[TableColumnsIdsKeys];
45
47
 
46
48
  interface StorageGroupsProps {
47
49
  data: any;
48
- nodes: any;
50
+ nodes: NodesMap;
49
51
  tableSettings: Settings;
50
52
  visibleEntities: keyof typeof VisibleEntities;
51
53
  onShowAll?: VoidFunction;
@@ -20,18 +20,19 @@
20
20
  margin-right: 0px;
21
21
  }
22
22
  }
23
- &__fqdn-wrapper {
23
+ &__fqdn-field-wrapper {
24
24
  width: 330px;
25
25
  }
26
- &__fqdn {
27
- display: inline-block;
28
- overflow: hidden;
26
+ &__fqdn-wrapper {
27
+ display: flex;
28
+ align-items: center;
29
29
 
30
30
  max-width: 330px;
31
-
32
- vertical-align: top;
33
- text-overflow: ellipsis;
34
31
  }
32
+ &__fqdn {
33
+ overflow: hidden;
34
+ }
35
+
35
36
  &__group-id {
36
37
  font-weight: 500;
37
38
  }
@@ -39,4 +40,16 @@
39
40
  &__node_unavailable {
40
41
  opacity: 0.6;
41
42
  }
43
+
44
+ &__external-button {
45
+ display: none;
46
+
47
+ margin-left: 4px;
48
+ }
49
+ }
50
+
51
+ .data-table__row:hover {
52
+ .global-storage-nodes__external-button {
53
+ display: inline-flex;
54
+ }
42
55
  }
@@ -2,10 +2,20 @@ import _ from 'lodash';
2
2
  import cn from 'bem-cn-lite';
3
3
 
4
4
  import DataTable, {Column, Settings, SortOrder} from '@gravity-ui/react-data-table';
5
- import {Popover, PopoverBehavior} from '@gravity-ui/uikit';
5
+ import {Button, Popover, PopoverBehavior} from '@gravity-ui/uikit';
6
+
7
+ import {NodeEndpointsTooltip} from '../../../components/Tooltips/NodeEndpointsTooltip/NodeEndpointsTooltip';
8
+ import EntityStatus from '../../../components/EntityStatus/EntityStatus';
9
+ import {IconWrapper} from '../../../components/Icon';
6
10
 
7
11
  import {VisibleEntities} from '../../../store/reducers/storage';
8
- import {isUnavailableNode, NodesUptimeFilterValues} from '../../../utils/nodes';
12
+ import {
13
+ AdditionalNodesInfo,
14
+ isUnavailableNode,
15
+ NodesUptimeFilterValues,
16
+ } from '../../../utils/nodes';
17
+
18
+ import {getDefaultNodePath} from '../../Node/NodePages';
9
19
 
10
20
  import {EmptyFilter} from '../EmptyFilter/EmptyFilter';
11
21
  import {PDisk} from '../PDisk';
@@ -33,6 +43,7 @@ interface StorageNodesProps {
33
43
  visibleEntities: keyof typeof VisibleEntities;
34
44
  nodesUptimeFilter: keyof typeof NodesUptimeFilterValues;
35
45
  onShowAll?: VoidFunction;
46
+ additionalNodesInfo?: AdditionalNodesInfo;
36
47
  }
37
48
 
38
49
  const tableColumnsNames: Record<TableColumnsIdsValues, string> = {
@@ -73,7 +84,10 @@ function StorageNodes({
73
84
  visibleEntities,
74
85
  onShowAll,
75
86
  nodesUptimeFilter,
87
+ additionalNodesInfo,
76
88
  }: StorageNodesProps) {
89
+ const getNodeRef = additionalNodesInfo?.getNodeRef;
90
+
77
91
  const allColumns: Column<any>[] = [
78
92
  {
79
93
  name: TableColumnsIds.NodeId,
@@ -85,15 +99,37 @@ function StorageNodes({
85
99
  name: TableColumnsIds.FQDN,
86
100
  header: tableColumnsNames[TableColumnsIds.FQDN],
87
101
  width: 350,
88
- render: ({value}) => {
102
+ render: ({row}) => {
103
+ const nodeRef = getNodeRef ? getNodeRef(row) + 'internal' : undefined;
104
+ if (!row.Host) {
105
+ return <span>—</span>;
106
+ }
89
107
  return (
90
- <div className={b('fqdn-wrapper')}>
108
+ <div className={b('fqdn-field-wrapper')}>
91
109
  <Popover
92
- content={value as string}
93
- placement={['right']}
110
+ content={<NodeEndpointsTooltip data={row} />}
111
+ placement={['top', 'bottom']}
94
112
  behavior={PopoverBehavior.Immediate}
95
113
  >
96
- <span className={b('fqdn')}>{value as string}</span>
114
+ <div className={b('fqdn-wrapper')}>
115
+ <EntityStatus
116
+ name={row.Host}
117
+ showStatus={false}
118
+ path={getDefaultNodePath(row.NodeId)}
119
+ hasClipboardButton
120
+ className={b('fqdn')}
121
+ />
122
+ {nodeRef && (
123
+ <Button
124
+ size="s"
125
+ href={nodeRef}
126
+ className={b('external-button')}
127
+ target="_blank"
128
+ >
129
+ <IconWrapper name="external" />
130
+ </Button>
131
+ )}
132
+ </div>
97
133
  </Popover>
98
134
  </div>
99
135
  );
@@ -1,6 +1,8 @@
1
1
  import React, {useEffect, useState, useRef, useMemo} from 'react';
2
2
  import cn from 'bem-cn-lite';
3
3
 
4
+ import type {NodesMap} from '../../../types/store/nodesList';
5
+
4
6
  import {InternalLink} from '../../../components/InternalLink';
5
7
 
6
8
  import routes, {createHref} from '../../../routes';
@@ -12,7 +14,6 @@ import {isFullVDiskData} from '../../../utils/storage';
12
14
  import {STRUCTURE} from '../../Node/NodePages';
13
15
 
14
16
  import {DiskStateProgressBar, EDiskStateSeverity} from '../DiskStateProgressBar';
15
- import type {NodesHosts} from '../PDiskPopup';
16
17
  import {VDiskPopup} from '../VDiskPopup';
17
18
 
18
19
  import type {IUnavailableDonor} from '../utils/types';
@@ -50,7 +51,7 @@ const getColorSeverity = (color?: EFlag) => {
50
51
  interface VDiskProps {
51
52
  data?: TVDiskStateInfo | IUnavailableDonor;
52
53
  poolName?: string;
53
- nodes?: NodesHosts;
54
+ nodes?: NodesMap;
54
55
  compact?: boolean;
55
56
  }
56
57
 
@@ -3,6 +3,8 @@ import cn from 'bem-cn-lite';
3
3
 
4
4
  import {Label, Popup, PopupProps} from '@gravity-ui/uikit';
5
5
 
6
+ import type {NodesMap} from '../../../types/store/nodesList';
7
+
6
8
  import {InfoViewer, InfoViewerItem} from '../../../components/InfoViewer';
7
9
 
8
10
  import {EFlag} from '../../../types/api/enums';
@@ -13,7 +15,7 @@ import {isFullVDiskData} from '../../../utils/storage';
13
15
 
14
16
  import type {IUnavailableDonor} from '../utils/types';
15
17
 
16
- import {NodesHosts, preparePDiskData} from '../PDiskPopup';
18
+ import {preparePDiskData} from '../PDiskPopup';
17
19
 
18
20
  import './VDiskPopup.scss';
19
21
 
@@ -128,7 +130,7 @@ const prepareVDiskData = (data: TVDiskStateInfo, poolName?: string) => {
128
130
  interface VDiskPopupProps extends PopupProps {
129
131
  data: TVDiskStateInfo | IUnavailableDonor;
130
132
  poolName?: string;
131
- nodes?: NodesHosts;
133
+ nodes?: NodesMap;
132
134
  }
133
135
 
134
136
  export const VDiskPopup = ({data, poolName, nodes, ...props}: VDiskPopupProps) => {
@@ -2,12 +2,4 @@
2
2
  &__lags {
3
3
  white-space: nowrap;
4
4
  }
5
-
6
- &__lags-popover-content {
7
- max-width: 300px;
8
-
9
- div:nth-child(1) {
10
- margin-bottom: 10px;
11
- }
12
- }
13
5
  }
@@ -1,7 +1,7 @@
1
1
  import block from 'bem-cn-lite';
2
2
 
3
- import {ReadLagImage} from '../../../../../components/LagImages';
4
3
  import {LabelWithPopover} from '../../../../../components/LabelWithPopover';
4
+ import {LagPopoverContent} from '../../../../../components/LagPopoverContent';
5
5
 
6
6
  import {CONSUMERS_COLUMNS_IDS, CONSUMERS_COLUMNS_TITILES} from '../utils/constants';
7
7
 
@@ -14,14 +14,7 @@ const b = block('ydb-diagnostics-consumers-columns-header');
14
14
  export const ReadLagsHeader = () => (
15
15
  <LabelWithPopover
16
16
  className={b('lags')}
17
- headerText={CONSUMERS_COLUMNS_TITILES[CONSUMERS_COLUMNS_IDS.READ_LAGS]}
18
- popoverContent={
19
- <div className={b('lags-popover-content')}>
20
- <div>{i18n('lagsPopover.readLags')}</div>
21
- <div>
22
- <ReadLagImage />
23
- </div>
24
- </div>
25
- }
17
+ text={CONSUMERS_COLUMNS_TITILES[CONSUMERS_COLUMNS_IDS.READ_LAGS]}
18
+ popoverContent={<LagPopoverContent text={i18n('lagsPopover.readLags')} type="read" />}
26
19
  />
27
20
  );
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "noConsumersMessage.topic": "У этого топика нет читателей",
3
3
  "noConsumersMessage.stream": "У этого стрима нет читателей",
4
- "lagsPopover.readLags": "Статистика лагов чтения, максимальной значение среди всех партиций читателя (формат времени дд чч:мм:сс)",
4
+ "lagsPopover.readLags": "Статистика лагов чтения, максимальное значение среди всех партиций читателя (формат времени дд чч:мм:сс)",
5
5
  "table.emptyDataMessage": "По заданному поиску нет читателей",
6
6
  "controls.search": "Consumer"
7
7
  }
@@ -131,7 +131,8 @@ function Diagnostics(props: DiagnosticsProps) {
131
131
  case GeneralPagesIds.nodes: {
132
132
  return (
133
133
  <Nodes
134
- tenantPath={tenantNameString}
134
+ path={currentSchemaPath}
135
+ type={type}
135
136
  additionalNodesInfo={props.additionalNodesInfo}
136
137
  />
137
138
  );
@@ -89,12 +89,12 @@ export const DATABASE_PAGES = [
89
89
  describe,
90
90
  ];
91
91
 
92
- export const TABLE_PAGES = [overview, topShards, graph, tablets, hotKeys, describe];
92
+ export const TABLE_PAGES = [overview, topShards, nodes, graph, tablets, hotKeys, describe];
93
93
 
94
- export const DIR_PAGES = [overview, topShards, describe];
94
+ export const DIR_PAGES = [overview, topShards, nodes, describe];
95
95
 
96
- export const CDC_STREAM_PAGES = [overview, consumers, partitions, describe];
97
- export const TOPIC_PAGES = [overview, consumers, partitions, describe];
96
+ export const CDC_STREAM_PAGES = [overview, consumers, partitions, nodes, describe];
97
+ export const TOPIC_PAGES = [overview, consumers, partitions, nodes, describe];
98
98
 
99
99
  // verbose mapping to guarantee correct tabs for new path types
100
100
  // TS will error when a new type is added but not mapped here
@@ -1,16 +1,19 @@
1
1
  import cn from 'bem-cn-lite';
2
- import {isEmpty} from 'lodash/fp';
3
2
 
4
- import type {DescribeTopicResult} from '../../../../../types/api/topic';
3
+ import type {IPreparedTopicStats} from '../../../../../types/store/topic';
5
4
 
6
5
  import {Loader} from '../../../../../components/Loader';
7
- import {InfoViewerItem, formatObject, InfoViewer} from '../../../../../components/InfoViewer';
8
-
9
- import {formatTopicStats} from '../../../../../components/InfoViewer/formatters';
6
+ import {InfoViewerItem, InfoViewer} from '../../../../../components/InfoViewer';
7
+ import {SpeedMultiMeter} from '../../../../../components/SpeedMultiMeter';
8
+ import {LabelWithPopover} from '../../../../../components/LabelWithPopover';
9
+ import {LagPopoverContent} from '../../../../../components/LagPopoverContent';
10
+ import {ResponseError} from '../../../../../components/Errors/ResponseError';
10
11
 
11
12
  import {useTypedSelector} from '../../../../../utils/hooks';
12
- import {convertBytesObjectToSpeed} from '../../../../../utils/bytesParsers';
13
- import {formatBps} from '../../../../../utils';
13
+ import {formatDurationToShortTimeFormat} from '../../../../../utils/timeParsers';
14
+ import {formatBps, formatBytes} from '../../../../../utils';
15
+
16
+ import {selectPreparedTopicStats} from '../../../../../store/reducers/topic';
14
17
 
15
18
  import i18n from './i18n';
16
19
 
@@ -18,35 +21,61 @@ import './TopicStats.scss';
18
21
 
19
22
  const b = cn('ydb-overview-topic-stats');
20
23
 
21
- const prepareTopicInfo = (data: DescribeTopicResult): Array<InfoViewerItem> => {
24
+ const prepareTopicInfo = (data: IPreparedTopicStats): Array<InfoViewerItem> => {
22
25
  return [
23
- ...formatObject(formatTopicStats, {
24
- ...data.topic_stats,
25
- }),
26
+ {label: 'Store size', value: formatBytes(data.storeSize)},
27
+ {
28
+ label: (
29
+ <LabelWithPopover
30
+ text={'Write idle time'}
31
+ popoverContent={
32
+ <LagPopoverContent text={i18n('writeIdleTimePopover')} type="write" />
33
+ }
34
+ />
35
+ ),
36
+ value: formatDurationToShortTimeFormat(data.partitionsIdleTime),
37
+ },
38
+ {
39
+ label: (
40
+ <LabelWithPopover
41
+ text={'Write lag'}
42
+ popoverContent={
43
+ <LagPopoverContent text={i18n('writeLagPopover')} type="write" />
44
+ }
45
+ />
46
+ ),
47
+ value: formatDurationToShortTimeFormat(data.partitionsWriteLag),
48
+ },
49
+ {
50
+ label: 'Average write speed',
51
+ value: <SpeedMultiMeter data={data.writeSpeed} withValue={false} />,
52
+ },
26
53
  ];
27
54
  };
28
55
 
29
- const prepareBytesWrittenInfo = (data: DescribeTopicResult): Array<InfoViewerItem> => {
30
- const preparedBytes = convertBytesObjectToSpeed(data?.topic_stats?.bytes_written);
56
+ const prepareBytesWrittenInfo = (data: IPreparedTopicStats): Array<InfoViewerItem> => {
57
+ const writeSpeed = data.writeSpeed;
31
58
 
32
59
  return [
33
60
  {
34
61
  label: 'per minute',
35
- value: formatBps(preparedBytes.perMinute),
62
+ value: formatBps(writeSpeed.perMinute),
36
63
  },
37
64
  {
38
65
  label: 'per hour',
39
- value: formatBps(preparedBytes.perHour),
66
+ value: formatBps(writeSpeed.perHour),
40
67
  },
41
68
  {
42
69
  label: 'per day',
43
- value: formatBps(preparedBytes.perDay),
70
+ value: formatBps(writeSpeed.perDay),
44
71
  },
45
72
  ];
46
73
  };
47
74
 
48
75
  export const TopicStats = () => {
49
- const {data, error, loading, wasLoaded} = useTypedSelector((state) => state.topic);
76
+ const {error, loading, wasLoaded} = useTypedSelector((state) => state.topic);
77
+
78
+ const data = useTypedSelector(selectPreparedTopicStats);
50
79
 
51
80
  if (loading && !wasLoaded) {
52
81
  return (
@@ -56,24 +85,14 @@ export const TopicStats = () => {
56
85
  );
57
86
  }
58
87
 
59
- // There are several backed versions with different behaviour
60
- // Possible returns on older versions:
61
- // 1. Error when trying to access endpoint
62
- // 2. No data
63
- // 3. HTML page of Internal Viewer with an error
64
- // 4. Data with no topic stats
65
- // 5. Topic Stats as an empty object
66
- if (
67
- error ||
68
- !data ||
69
- typeof data !== 'object' ||
70
- !data.topic_stats ||
71
- isEmpty(data.topic_stats)
72
- ) {
88
+ // If there is at least some empty data object
89
+ // we initialize its fields with zero values
90
+ // so no data at all is considered to be error as well
91
+ if (error || !data) {
73
92
  return (
74
93
  <div className={b()}>
75
94
  <div className={b('title')}>Stats</div>
76
- <div className="error">{i18n('notSupportedVersion')}</div>
95
+ <ResponseError error={error} />
77
96
  </div>
78
97
  );
79
98
  }
@@ -1,3 +1,4 @@
1
1
  {
2
- "notSupportedVersion": "Topic stats are not supported in the current cluster version. Update cluster version to see topic stats"
2
+ "writeLagPopover": "Write lag, maximum among all topic partitions",
3
+ "writeIdleTimePopover": "Write idle time, maximum among all topic partitions"
3
4
  }
@@ -1,3 +1,4 @@
1
1
  {
2
- "notSupportedVersion": "Статистика топиков не поддерживается в текущей версии. Обновите кластер до более новой версии, чтобы увидеть статистику топиков"
2
+ "writeLagPopover": "Лаг записи, максимальное значение среди всех партиций топика",
3
+ "writeIdleTimePopover": "Время без записи, максимальное значение среди всех партиций топика"
3
4
  }
@@ -19,14 +19,6 @@
19
19
  white-space: normal;
20
20
  }
21
21
 
22
- &__lags-popover-content {
23
- max-width: 300px;
24
-
25
- div:nth-child(1) {
26
- margin-bottom: 10px;
27
- }
28
- }
29
-
30
22
  &__messages-popover-content {
31
23
  max-width: 200px;
32
24
  }
@@ -1,7 +1,7 @@
1
1
  import block from 'bem-cn-lite';
2
2
 
3
3
  import {LabelWithPopover} from '../../../../../components/LabelWithPopover';
4
- import {WriteLagImage, ReadLagImage} from '../../../../../components/LagImages';
4
+ import {LagPopoverContent} from '../../../../../components/LagPopoverContent';
5
5
 
6
6
  import {PARTITIONS_COLUMNS_IDS, PARTITIONS_COLUMNS_TITILES} from '../utils/constants';
7
7
 
@@ -28,37 +28,23 @@ export const ReadSessionHeader = () => (
28
28
  export const WriteLagsHeader = () => (
29
29
  <LabelWithPopover
30
30
  className={b('lags')}
31
- headerText={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.READ_LAGS]}
32
- popoverContent={
33
- <div className={b('lags-popover-content')}>
34
- <div>{i18n('lagsPopover.writeLags')}</div>
35
- <div>
36
- <WriteLagImage />
37
- </div>
38
- </div>
39
- }
31
+ text={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.READ_LAGS]}
32
+ popoverContent={<LagPopoverContent text={i18n('lagsPopover.readLags')} type="read" />}
40
33
  />
41
34
  );
42
35
 
43
36
  export const ReadLagsHeader = () => (
44
37
  <LabelWithPopover
45
38
  className={b('lags')}
46
- headerText={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.WRITE_LAGS]}
47
- popoverContent={
48
- <div className={b('lags-popover-content')}>
49
- <div>{i18n('lagsPopover.readLags')}</div>
50
- <div>
51
- <ReadLagImage />
52
- </div>
53
- </div>
54
- }
39
+ text={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.WRITE_LAGS]}
40
+ popoverContent={<LagPopoverContent text={i18n('lagsPopover.writeLags')} type="write" />}
55
41
  />
56
42
  );
57
43
 
58
44
  export const UnreadMessagesHeader = () => (
59
45
  <LabelWithPopover
60
46
  className={b('messages')}
61
- headerText={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.UNREAD_MESSAGES]}
47
+ text={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.UNREAD_MESSAGES]}
62
48
  popoverContent={
63
49
  <div className={b('messages-popover-content')}>{i18n('headers.unread')}</div>
64
50
  }
@@ -68,7 +54,7 @@ export const UnreadMessagesHeader = () => (
68
54
  export const UncommitedMessagesHeader = () => (
69
55
  <LabelWithPopover
70
56
  className={b('messages')}
71
- headerText={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.UNCOMMITED_MESSAGES]}
57
+ text={PARTITIONS_COLUMNS_TITILES[PARTITIONS_COLUMNS_IDS.UNCOMMITED_MESSAGES]}
72
58
  popoverContent={
73
59
  <div className={b('messages-popover-content')}>{i18n('headers.uncommited')}</div>
74
60
  }
@@ -77,6 +77,10 @@ interface Window {
77
77
  consumer?: string;
78
78
  }) => Promise<import('../types/api/consumer').DescribeConsumerResult>;
79
79
  getHostInfo: () => Promise<import('../types/api/systemState').TEvSystemStateResponse>;
80
+ getNodeInfo: (
81
+ id?: string,
82
+ ) => Promise<import('../types/api/systemState').TEvSystemStateResponse>;
83
+ getNodesList: () => Promise<import('../types/api/nodesList').TEvNodesInfo>;
80
84
  [method: string]: Function;
81
85
  };
82
86
  }
@@ -1,11 +1,19 @@
1
+ import type {Reducer} from 'redux';
2
+
3
+ import type {
4
+ NodesListState,
5
+ NodesListAction,
6
+ NodesListRootStateSlice,
7
+ NodesMap,
8
+ } from '../../types/store/nodesList';
1
9
  import '../../services/api';
2
10
  import {createRequestActionTypes, createApiRequest} from '../utils';
3
11
 
4
- const FETCH_NODES_LIST = createRequestActionTypes('tenants', 'FETCH_NODES_LIST');
12
+ export const FETCH_NODES_LIST = createRequestActionTypes('nodesList', 'FETCH_NODES_LIST');
5
13
 
6
14
  const initialState = {loading: true, wasLoaded: false, data: []};
7
15
 
8
- const nodesList = function (state = initialState, action) {
16
+ const nodesList: Reducer<NodesListState, NodesListAction> = (state = initialState, action) => {
9
17
  switch (action.type) {
10
18
  case FETCH_NODES_LIST.REQUEST: {
11
19
  return {
@@ -36,13 +44,17 @@ const nodesList = function (state = initialState, action) {
36
44
 
37
45
  export function getNodesList() {
38
46
  return createApiRequest({
39
- request: window.api.getNodeInfo(),
47
+ request: window.api.getNodesList(),
40
48
  actions: FETCH_NODES_LIST,
41
- dataHandler: (data) => {
42
- const {SystemStateInfo: nodes = []} = data;
43
- return nodes;
44
- },
45
49
  });
46
50
  }
47
51
 
52
+ export const selectNodesMap = (state: NodesListRootStateSlice) =>
53
+ state.nodesList.data?.reduce<NodesMap>((nodesMap, node) => {
54
+ if (node.Id && node.Host) {
55
+ nodesMap.set(node.Id, node.Host);
56
+ }
57
+ return nodesMap;
58
+ }, new Map());
59
+
48
60
  export default nodesList;
@@ -204,15 +204,6 @@ export const getUsageFilter = (state) => state.storage.usageFilter;
204
204
  export const getVisibleEntities = (state) => state.storage.visible;
205
205
  export const getNodesUptimeFilter = (state) => state.storage.nodesUptimeFilter;
206
206
  export const getStorageType = (state) => state.storage.type;
207
- export const getNodesObject = (state) =>
208
- _.reduce(
209
- state.nodesList?.data,
210
- (acc, el) => {
211
- acc[el.NodeId] = el.Host;
212
- return acc;
213
- },
214
- {},
215
- );
216
207
 
217
208
  const FLAGS_POINTS = {
218
209
  Green: 1,
@@ -314,9 +305,10 @@ export const getFlatListStorageNodes = createSelector([getStorageNodes], (storag
314
305
  return {
315
306
  NodeId: node.NodeId,
316
307
  SystemState: systemState.SystemState,
317
- FQDN: systemState.Host,
318
308
  DataCenter: systemState.DataCenter,
319
309
  Rack: systemState.Rack,
310
+ Host: systemState.Host,
311
+ Endpoints: systemState.Endpoints,
320
312
  uptime: calcUptime(systemState.StartTime),
321
313
  StartTime: systemState.StartTime,
322
314
  PDisks: node.PDisks,
@@ -378,7 +370,7 @@ const filterByText = (entities, type, text) => {
378
370
 
379
371
  return (
380
372
  entity.NodeId.toString().includes(cleanedFilter) ||
381
- entity.FQDN.toLowerCase().includes(cleanedFilter)
373
+ entity.Host.toLowerCase().includes(cleanedFilter)
382
374
  );
383
375
  });
384
376
  };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * endpoint: /viewer/json/nodesList
3
+ *
4
+ * source: https://github.com/ydb-platform/ydb/blob/main/library/cpp/actors/core/interconnect.h
5
+ */
6
+ export type TEvNodesInfo = TNodeInfo[];
7
+
8
+ export interface TNodeInfo {
9
+ Id?: number;
10
+ Host?: string;
11
+ ResolveHost?: string;
12
+ Address?: string;
13
+ Port?: number;
14
+ PhysicalLocation?: TNodeLocation;
15
+ }
16
+
17
+ interface TNodeLocation {
18
+ DataCenter?: number;
19
+ Room?: number;
20
+ Rack?: number;
21
+ Body?: number;
22
+ DataCenterId?: string;
23
+ /** String with DC, Module, Rack and Unit ids */
24
+ Location?: string;
25
+ }
@@ -0,0 +1,24 @@
1
+ import {FETCH_NODES_LIST} from '../../store/reducers/nodesList';
2
+
3
+ import type {ApiRequestAction} from '../../store/utils';
4
+ import type {IResponseError} from '../api/error';
5
+ import type {TEvNodesInfo} from '../api/nodesList';
6
+
7
+ export interface NodesListState {
8
+ loading: boolean;
9
+ wasLoaded: boolean;
10
+ data?: TEvNodesInfo;
11
+ error?: IResponseError;
12
+ }
13
+
14
+ export type NodesListAction = ApiRequestAction<
15
+ typeof FETCH_NODES_LIST,
16
+ TEvNodesInfo,
17
+ IResponseError
18
+ >;
19
+
20
+ export type NodesMap = Map<number, string>;
21
+
22
+ export interface NodesListRootStateSlice {
23
+ nodesList: NodesListState;
24
+ }
@@ -20,7 +20,15 @@ export const formatBytes = (bytes) => {
20
20
  return numeral(bytes).format('0 b');
21
21
  };
22
22
 
23
- export const formatBps = (bytes) => formatBytes(bytes) + '/s';
23
+ export const formatBps = (bytes) => {
24
+ const formattedBytes = formatBytes(bytes);
25
+
26
+ if (!formattedBytes) {
27
+ return '';
28
+ }
29
+
30
+ return formattedBytes + '/s';
31
+ };
24
32
 
25
33
  export const formatBytesToGigabyte = (bytes) => {
26
34
  return `${Math.floor(bytes / GIGABYTE)} GB`;
@@ -14,3 +14,7 @@ export const NodesUptimeFilterTitles = {
14
14
 
15
15
  export const isUnavailableNode = (node: INodesPreparedEntity | TSystemStateInfo) =>
16
16
  !node.SystemState || node.SystemState === EFlag.Grey;
17
+
18
+ export interface AdditionalNodesInfo extends Record<string, unknown> {
19
+ getNodeRef?: Function;
20
+ }
@@ -2,6 +2,8 @@ import cn from 'bem-cn-lite';
2
2
  import {calcUptime} from '.';
3
3
  import JSONTree from 'react-json-inspector';
4
4
 
5
+ import {NodeEndpointsTooltip} from '../components/Tooltips/NodeEndpointsTooltip/NodeEndpointsTooltip';
6
+
5
7
  const poolB = cn('pool-tooltip');
6
8
 
7
9
  const PoolTooltip = (props) => {
@@ -109,34 +111,6 @@ const NodeTooltip = (props) => {
109
111
  );
110
112
  };
111
113
 
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 &&
126
- data.Endpoints.length &&
127
- data.Endpoints.map(({Name, Address}) => (
128
- <tr key={Name}>
129
- <td className={nodeB('label')}>{Name}</td>
130
- <td className={nodeB('value')}>{Address}</td>
131
- </tr>
132
- ))}
133
- </tbody>
134
- </table>
135
- </div>
136
- )
137
- );
138
- };
139
-
140
114
  const tabletsOverallB = cn('tabletsOverall-tooltip');
141
115
 
142
116
  const TabletsOverallTooltip = (props) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ydb-embedded-ui",
3
- "version": "3.4.3",
3
+ "version": "3.4.5",
4
4
  "files": [
5
5
  "dist"
6
6
  ],
@@ -1,29 +0,0 @@
1
- import type {TopicStats} from '../../../types/api/topic';
2
- import {formatBytes} from '../../../utils';
3
- import {
4
- parseLag,
5
- parseTimestampToIdleTime,
6
- formatDurationToShortTimeFormat,
7
- } from '../../../utils/timeParsers';
8
- import {convertBytesObjectToSpeed} from '../../../utils/bytesParsers';
9
-
10
- import {SpeedMultiMeter} from '../../SpeedMultiMeter';
11
-
12
- import {createInfoFormatter} from '../utils';
13
-
14
- export const formatTopicStats = createInfoFormatter<TopicStats>({
15
- values: {
16
- store_size_bytes: formatBytes,
17
- min_last_write_time: (value) =>
18
- formatDurationToShortTimeFormat(parseTimestampToIdleTime(value)),
19
- max_write_time_lag: (value) => formatDurationToShortTimeFormat(parseLag(value)),
20
- bytes_written: (value) =>
21
- value && <SpeedMultiMeter data={convertBytesObjectToSpeed(value)} withValue={false} />,
22
- },
23
- labels: {
24
- store_size_bytes: 'Store size',
25
- min_last_write_time: 'Partitions max time since last write',
26
- max_write_time_lag: 'Partitions max write time lag',
27
- bytes_written: 'Average write speed',
28
- },
29
- });