ydb-embedded-ui 4.6.0 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/icons/versions.svg +3 -0
  3. package/dist/components/NodeHostWrapper/NodeHostWrapper.tsx +6 -1
  4. package/dist/components/Tablet/Tablet.tsx +17 -3
  5. package/dist/components/TabletsStatistic/TabletsStatistic.tsx +23 -16
  6. package/dist/containers/App/Content.js +5 -2
  7. package/dist/containers/Cluster/Cluster.tsx +6 -13
  8. package/dist/containers/Cluster/ClusterInfo/ClusterInfo.tsx +3 -3
  9. package/dist/containers/Cluster/{utils.ts → utils.tsx} +11 -0
  10. package/dist/containers/Header/Header.scss +9 -0
  11. package/dist/containers/Header/Header.tsx +70 -14
  12. package/dist/containers/Header/breadcrumbs.ts +146 -0
  13. package/dist/containers/Node/Node.tsx +21 -27
  14. package/dist/containers/Node/NodePages.ts +10 -6
  15. package/dist/containers/Storage/StorageGroups/StorageGroups.tsx +11 -3
  16. package/dist/containers/Tablet/Tablet.tsx +35 -27
  17. package/dist/containers/Tablet/TabletInfo/TabletInfo.tsx +2 -2
  18. package/dist/containers/TabletsFilters/TabletsFilters.js +13 -15
  19. package/dist/containers/Tenant/Diagnostics/Consumers/columns/columns.tsx +1 -1
  20. package/dist/containers/Tenant/Diagnostics/DetailedOverview/DetailedOverview.scss +5 -1
  21. package/dist/containers/Tenant/Diagnostics/Diagnostics.tsx +1 -1
  22. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.scss +5 -3
  23. package/dist/containers/Tenant/Diagnostics/Healthcheck/Healthcheck.tsx +1 -1
  24. package/dist/containers/Tenant/Diagnostics/TenantOverview/TenantOverview.js +2 -1
  25. package/dist/containers/Tenant/Diagnostics/TopQueries/TopQueries.tsx +11 -13
  26. package/dist/containers/Tenant/ObjectGeneral/ObjectGeneral.tsx +2 -2
  27. package/dist/containers/Tenant/ObjectSummary/ObjectSummary.tsx +6 -2
  28. package/dist/containers/Tenant/{QueryEditor/QueryResult/QueryResult.js → Query/ExecuteResult/ExecuteResult.js} +3 -5
  29. package/dist/containers/Tenant/{QueryEditor/QueryResult/QueryResult.scss → Query/ExecuteResult/ExecuteResult.scss} +1 -1
  30. package/dist/containers/Tenant/{QueryEditor/QueryExplain/QueryExplain.js → Query/ExplainResult/ExplainResult.js} +3 -5
  31. package/dist/containers/Tenant/{QueryEditor/QueryExplain/QueryExplain.scss → Query/ExplainResult/ExplainResult.scss} +1 -1
  32. package/dist/containers/Tenant/Query/QueriesHistory/QueriesHistory.scss +20 -0
  33. package/dist/containers/Tenant/Query/QueriesHistory/QueriesHistory.tsx +60 -0
  34. package/dist/containers/Tenant/Query/Query.scss +16 -0
  35. package/dist/containers/Tenant/Query/Query.tsx +73 -0
  36. package/dist/containers/Tenant/{QueryEditor → Query/QueryEditor}/QueryEditor.js +26 -87
  37. package/dist/containers/Tenant/{QueryEditor → Query/QueryEditor}/QueryEditor.scss +7 -23
  38. package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/QueryEditorControls.scss +1 -4
  39. package/dist/containers/Tenant/Query/QueryTabs/QueryTabs.tsx +59 -0
  40. package/dist/containers/Tenant/{QueryEditor → Query}/SaveQuery/SaveQuery.js +5 -5
  41. package/dist/containers/Tenant/Query/SavedQueries/SavedQueries.scss +55 -0
  42. package/dist/containers/Tenant/Query/SavedQueries/SavedQueries.tsx +150 -0
  43. package/dist/containers/Tenant/Query/i18n/en.json +12 -0
  44. package/dist/containers/Tenant/Query/i18n/ru.json +12 -0
  45. package/dist/containers/Tenant/Query/utils/getPreparedResult.ts +30 -0
  46. package/dist/containers/Tenant/Tenant.tsx +2 -18
  47. package/dist/containers/Tenant/TenantPages.tsx +8 -2
  48. package/dist/containers/Tenant/utils/constants.ts +10 -0
  49. package/dist/containers/Tenant/utils/schemaActions.ts +7 -2
  50. package/dist/containers/Tenants/Tenants.js +21 -9
  51. package/dist/routes.ts +10 -1
  52. package/dist/services/api.ts +9 -5
  53. package/dist/store/reducers/executeQuery.ts +1 -1
  54. package/dist/store/reducers/header/header.ts +31 -0
  55. package/dist/store/reducers/header/types.ts +54 -0
  56. package/dist/store/reducers/index.ts +1 -1
  57. package/dist/store/reducers/node/types.ts +2 -0
  58. package/dist/store/reducers/settings/settings.ts +1 -1
  59. package/dist/store/reducers/tablet.ts +18 -1
  60. package/dist/store/reducers/tenant/constants.ts +6 -0
  61. package/dist/store/reducers/tenant/tenant.ts +21 -2
  62. package/dist/store/reducers/tenant/types.ts +9 -2
  63. package/dist/store/reducers/topic.ts +1 -1
  64. package/dist/store/state-url-mapping.js +4 -1
  65. package/dist/types/store/query.ts +5 -0
  66. package/dist/types/store/tablet.ts +7 -4
  67. package/dist/utils/constants.ts +2 -0
  68. package/package.json +2 -1
  69. package/dist/containers/Tenant/QueryEditor/QueriesHistory/QueriesHistory.scss +0 -85
  70. package/dist/containers/Tenant/QueryEditor/QueriesHistory/QueriesHistory.tsx +0 -95
  71. package/dist/containers/Tenant/QueryEditor/SavedQueries/SavedQueries.js +0 -161
  72. package/dist/containers/Tenant/QueryEditor/SavedQueries/SavedQueries.scss +0 -93
  73. package/dist/containers/Tenant/QueryEditor/i18n/en.json +0 -3
  74. package/dist/containers/Tenant/QueryEditor/i18n/ru.json +0 -3
  75. package/dist/store/reducers/header.ts +0 -26
  76. /package/dist/containers/Tenant/{QueryEditor → Query}/Issues/Issues.scss +0 -0
  77. /package/dist/containers/Tenant/{QueryEditor → Query}/Issues/Issues.tsx +0 -0
  78. /package/dist/containers/Tenant/{QueryEditor → Query}/Issues/models.ts +0 -0
  79. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryDuration/QueryDuration.scss +0 -0
  80. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryDuration/QueryDuration.tsx +0 -0
  81. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/OldQueryEditorControls.tsx +0 -0
  82. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/QueryEditorControls.tsx +0 -0
  83. /package/dist/containers/Tenant/{QueryEditor → Query}/QueryEditorControls/shared.ts +0 -0
  84. /package/dist/containers/Tenant/{QueryEditor → Query}/SaveQuery/SaveQuery.scss +0 -0
  85. /package/dist/containers/Tenant/{QueryEditor → Query}/i18n/index.ts +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [4.7.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.6.0...v4.7.0) (2023-06-23)
4
+
5
+
6
+ ### Features
7
+
8
+ * **QueryEditor:** transform history and saved to tabs ([#427](https://github.com/ydb-platform/ydb-embedded-ui/issues/427)) ([6378ca7](https://github.com/ydb-platform/ydb-embedded-ui/commit/6378ca7013239b33e55c1f88fdde7cab3a102df6))
9
+ * update breadcrumbs ([#432](https://github.com/ydb-platform/ydb-embedded-ui/issues/432)) ([e583a03](https://github.com/ydb-platform/ydb-embedded-ui/commit/e583a03fe0d77698f29c924e611133f015c3f7ad))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **Cluster:** add icons to tabs ([#430](https://github.com/ydb-platform/ydb-embedded-ui/issues/430)) ([e9e649f](https://github.com/ydb-platform/ydb-embedded-ui/commit/e9e649f614691e44172c9b93dd3119066c145413))
15
+ * **ClusterInfo:** hide by default ([#435](https://github.com/ydb-platform/ydb-embedded-ui/issues/435)) ([ef2b353](https://github.com/ydb-platform/ydb-embedded-ui/commit/ef2b3535f2c6324a34c4386680f5050655a04eb4))
16
+ * **Cluster:** use counter from uikit for tabs ([#428](https://github.com/ydb-platform/ydb-embedded-ui/issues/428)) ([19ca3bd](https://github.com/ydb-platform/ydb-embedded-ui/commit/19ca3bd14b15bdab1a9621939ddceee6d23b08ac))
17
+ * **DetailedOverview:** prevent tenant info scroll on overflow ([#434](https://github.com/ydb-platform/ydb-embedded-ui/issues/434)) ([8ed6076](https://github.com/ydb-platform/ydb-embedded-ui/commit/8ed60760d54913d05f39d35d00a34c8b1d7d9738))
18
+ * rename Internal Viewer to Developer UI ([#423](https://github.com/ydb-platform/ydb-embedded-ui/issues/423)) ([3eb21f3](https://github.com/ydb-platform/ydb-embedded-ui/commit/3eb21f35a230cc591f02ef9b195f99031f832e8a))
19
+ * **Storage:** update columns ([#437](https://github.com/ydb-platform/ydb-embedded-ui/issues/437)) ([264fbc9](https://github.com/ydb-platform/ydb-embedded-ui/commit/264fbc984cd9ef1467110d3e2f5ed9b29a526c2b))
20
+ * **Tablet:** clear tablet data on unmount ([#425](https://github.com/ydb-platform/ydb-embedded-ui/issues/425)) ([5d308cd](https://github.com/ydb-platform/ydb-embedded-ui/commit/5d308cdded342d7a40cbc6a91431d3f286c39b8a))
21
+ * **TabletsStatistic:** use tenant backend ([#429](https://github.com/ydb-platform/ydb-embedded-ui/issues/429)) ([d290684](https://github.com/ydb-platform/ydb-embedded-ui/commit/d290684ba08aec8b66c0492ba571a5337b5b896c))
22
+
3
23
  ## [4.6.0](https://github.com/ydb-platform/ydb-embedded-ui/compare/v4.5.2...v4.6.0) (2023-06-13)
4
24
 
5
25
 
@@ -0,0 +1,3 @@
1
+ <svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M8.625 13.0156C8.625 13.2695 8.47266 13.498 8.24414 13.5742C8.04102 13.6758 7.76172 13.6504 7.58398 13.4727L5.55273 11.6445C5.42578 11.543 5.375 11.3652 5.375 11.1875C5.375 11.0352 5.42578 10.8574 5.55273 10.7559L7.58398 8.92773C7.76172 8.75 8.04102 8.72461 8.24414 8.82617C8.47266 8.90234 8.625 9.13086 8.625 9.35938V10.5781H9.03125C10.0215 10.5781 10.8594 9.76562 10.8594 8.75V4.61133C10.0215 4.35742 9.4375 3.57031 9.4375 2.65625C9.4375 1.53906 10.3262 0.625 11.4688 0.625C12.5859 0.625 13.5 1.53906 13.5 2.65625C13.5 3.57031 12.8906 4.35742 12.0781 4.61133V8.75C12.0781 10.4512 10.707 11.7969 9.03125 11.7969H8.625V13.0156ZM12.2812 2.65625C12.2812 2.22461 11.9004 1.84375 11.4688 1.84375C11.0117 1.84375 10.6562 2.22461 10.6562 2.65625C10.6562 3.11328 11.0117 3.46875 11.4688 3.46875C11.9004 3.46875 12.2812 3.11328 12.2812 2.65625ZM5.375 1.23438C5.375 1.00586 5.50195 0.777344 5.73047 0.701172C5.93359 0.599609 6.21289 0.625 6.39062 0.802734L8.42188 2.63086C8.54883 2.73242 8.625 2.91016 8.625 3.0625C8.625 3.24023 8.54883 3.41797 8.42188 3.51953L6.39062 5.34766C6.21289 5.52539 5.93359 5.55078 5.73047 5.44922C5.50195 5.37305 5.375 5.14453 5.375 4.89062V3.67188H4.96875C3.95312 3.67188 3.14062 4.50977 3.14062 5.5V9.66406C3.95312 9.91797 4.5625 10.7051 4.5625 11.5938C4.5625 12.7363 3.64844 13.625 2.53125 13.625C1.38867 13.625 0.5 12.7363 0.5 11.5938C0.5 10.7051 1.08398 9.91797 1.92188 9.66406V5.5C1.92188 3.82422 3.26758 2.45312 4.96875 2.45312H5.375V1.23438ZM1.71875 11.5938C1.71875 12.0508 2.07422 12.4062 2.53125 12.4062C2.96289 12.4062 3.34375 12.0508 3.34375 11.5938C3.34375 11.1621 2.96289 10.7812 2.53125 10.7812C2.07422 10.7812 1.71875 11.1621 1.71875 11.5938Z"/>
3
+ </svg>
@@ -26,6 +26,11 @@ export const NodeHostWrapper = ({node, getNodeRef}: NodeHostWrapperProps) => {
26
26
 
27
27
  const isNodeAvailable = !isUnavailableNode(node);
28
28
  const nodeRef = isNodeAvailable && getNodeRef ? getNodeRef(node) + 'internal' : undefined;
29
+ const nodePath = isNodeAvailable
30
+ ? getDefaultNodePath(node.NodeId, {
31
+ tenantName: node.TenantName,
32
+ })
33
+ : undefined;
29
34
 
30
35
  return (
31
36
  <div className={b()}>
@@ -39,7 +44,7 @@ export const NodeHostWrapper = ({node, getNodeRef}: NodeHostWrapperProps) => {
39
44
  <EntityStatus
40
45
  name={node.Host}
41
46
  status={node.SystemState}
42
- path={isNodeAvailable ? getDefaultNodePath(node.NodeId) : undefined}
47
+ path={nodePath}
43
48
  hasClipboardButton
44
49
  className={b('host')}
45
50
  />
@@ -14,18 +14,32 @@ const b = cn('tablet');
14
14
 
15
15
  interface TabletProps {
16
16
  tablet?: TTabletStateInfo;
17
+ tenantName?: string;
17
18
  }
18
19
 
19
- export const Tablet = ({tablet = {}}: TabletProps) => {
20
- const {TabletId: id} = tablet;
20
+ export const Tablet = ({tablet = {}, tenantName}: TabletProps) => {
21
+ const {TabletId: id, NodeId, Type, State} = tablet;
21
22
  const status = tablet.Overall?.toLowerCase();
22
23
 
24
+ const tabletPath =
25
+ id &&
26
+ createHref(
27
+ routes.tablet,
28
+ {id},
29
+ {
30
+ nodeId: NodeId,
31
+ type: Type,
32
+ state: State,
33
+ tenantName,
34
+ },
35
+ );
36
+
23
37
  return (
24
38
  <ContentWithPopup
25
39
  className={b('wrapper')}
26
40
  content={<TabletTooltipContent data={tablet} className={b('popup-content')} />}
27
41
  >
28
- <InternalLink to={id && createHref(routes.tablet, {id})}>
42
+ <InternalLink to={tabletPath}>
29
43
  <div className={b({status})}>
30
44
  <div className={b('type')}>{[getTabletLabel(tablet.Type)]}</div>
31
45
  </div>
@@ -12,9 +12,9 @@ import './TabletsStatistic.scss';
12
12
 
13
13
  const b = cn('tablets-statistic');
14
14
 
15
- type ITablets = TFullTabletStateInfo[] | TComputeTabletStateInfo[];
15
+ type Tablets = TFullTabletStateInfo[] | TComputeTabletStateInfo[];
16
16
 
17
- const prepareTablets = (tablets: ITablets) => {
17
+ const prepareTablets = (tablets: Tablets) => {
18
18
  const res = tablets.map((tablet) => {
19
19
  return {
20
20
  label: getTabletLabel(tablet.Type),
@@ -28,25 +28,32 @@ const prepareTablets = (tablets: ITablets) => {
28
28
  };
29
29
 
30
30
  interface TabletsStatisticProps {
31
- tablets: ITablets;
31
+ tablets: Tablets;
32
32
  path: string | undefined;
33
33
  nodeIds: string[] | number[];
34
+ backend?: string;
34
35
  }
35
36
 
36
- export const TabletsStatistic = ({tablets = [], path, nodeIds}: TabletsStatisticProps) => {
37
+ export const TabletsStatistic = ({tablets = [], path, nodeIds, backend}: TabletsStatisticProps) => {
37
38
  const renderTabletInfo = (item: ReturnType<typeof prepareTablets>[number], index: number) => {
38
- return (
39
- <Link
40
- to={createHref(routes.tabletsFilters, undefined, {
41
- nodeIds,
42
- state: item.state,
43
- type: item.type,
44
- path,
45
- })}
46
- key={index}
47
- className={b('tablet', {state: item.state?.toLowerCase()})}
48
- >
49
- {item.label}: {item.count}
39
+ const tabletsPath = createHref(routes.tabletsFilters, undefined, {
40
+ nodeIds,
41
+ state: item.state,
42
+ type: item.type,
43
+ path,
44
+ backend,
45
+ });
46
+
47
+ const label = `${item.label}: ${item.count}`;
48
+ const className = b('tablet', {state: item.state?.toLowerCase()});
49
+
50
+ return backend ? (
51
+ <a href={tabletsPath} key={index} className={className}>
52
+ {label}
53
+ </a>
54
+ ) : (
55
+ <Link to={tabletsPath} key={index} className={className}>
56
+ {label}
50
57
  </Link>
51
58
  );
52
59
  };
@@ -1,5 +1,5 @@
1
1
  import React from 'react';
2
- import {Switch, Route, Redirect, Router} from 'react-router-dom';
2
+ import {Switch, Route, Redirect, Router, useLocation} from 'react-router-dom';
3
3
  import cn from 'bem-cn-lite';
4
4
  import {connect} from 'react-redux';
5
5
 
@@ -28,6 +28,8 @@ import {clusterTabsIds} from '../Cluster/utils';
28
28
  const b = cn('app');
29
29
 
30
30
  export function Content(props) {
31
+ const location = useLocation();
32
+
31
33
  const {singleClusterMode} = props;
32
34
  const isClustersPage =
33
35
  location.pathname.includes('/clusters') ||
@@ -54,7 +56,7 @@ export function Content(props) {
54
56
  };
55
57
  return (
56
58
  <React.Fragment>
57
- {!isClustersPage && <Header clusterName={props.clusterName} />}
59
+ {!isClustersPage && <Header mainPage={props.mainPage} />}
58
60
  <main className={b('main')}>{renderRoute()}</main>
59
61
  <ReduxTooltip />
60
62
  <AppIcons />
@@ -66,6 +68,7 @@ Content.propTypes = {
66
68
  singleClusterMode: PropTypes.bool,
67
69
  children: PropTypes.node,
68
70
  clusterName: PropTypes.string,
71
+ mainPage: PropTypes.object,
69
72
  };
70
73
 
71
74
  function ContentWrapper(props) {
@@ -10,7 +10,7 @@ import type {AdditionalClusterProps, AdditionalVersionsProps} from '../../types/
10
10
  import type {AdditionalNodesInfo} from '../../utils/nodes';
11
11
  import routes from '../../routes';
12
12
 
13
- import {setHeader} from '../../store/reducers/header';
13
+ import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
14
14
  import {getClusterInfo} from '../../store/reducers/cluster/cluster';
15
15
  import {getClusterNodes} from '../../store/reducers/clusterNodes/clusterNodes';
16
16
  import {parseNodesToVersionsValues, parseVersionsToVersionToColorMap} from '../../utils/versions';
@@ -80,14 +80,7 @@ function Cluster({
80
80
  );
81
81
 
82
82
  useEffect(() => {
83
- dispatch(
84
- setHeader([
85
- {
86
- text: Name || 'Cluster',
87
- link: getClusterPath(),
88
- },
89
- ]),
90
- );
83
+ dispatch(setHeaderBreadcrumbs('cluster', {}));
91
84
  }, [dispatch, Name]);
92
85
 
93
86
  const versionToColor = useMemo(() => {
@@ -124,13 +117,13 @@ function Cluster({
124
117
  const getTabEntityCount = (tabId: ClusterTab) => {
125
118
  switch (tabId) {
126
119
  case clusterTabsIds.tenants: {
127
- return cluster?.Tenants;
120
+ return cluster?.Tenants ? Number(cluster.Tenants) : undefined;
128
121
  }
129
122
  case clusterTabsIds.nodes: {
130
- return cluster?.NodesTotal;
123
+ return cluster?.NodesTotal ? Number(cluster.NodesTotal) : undefined;
131
124
  }
132
125
  default: {
133
- return null;
126
+ return undefined;
134
127
  }
135
128
  }
136
129
  };
@@ -153,7 +146,7 @@ function Cluster({
153
146
  items={clusterTabs.map((item) => {
154
147
  return {
155
148
  ...item,
156
- title: `${item.title} ${getTabEntityCount(item.id) || ''}`,
149
+ counter: getTabEntityCount(item.id),
157
150
  };
158
151
  })}
159
152
  wrapTo={({id}, node) => {
@@ -18,7 +18,7 @@ import type {TClusterInfo} from '../../../types/api/cluster';
18
18
  import {backend, customBackend} from '../../../store';
19
19
  import {formatStorageValues} from '../../../utils';
20
20
  import {useSetting, useTypedSelector} from '../../../utils/hooks';
21
- import {CLUSTER_INFO_HIDDEN_KEY} from '../../../utils/constants';
21
+ import {CLUSTER_INFO_HIDDEN_KEY, DEVELOPER_UI} from '../../../utils/constants';
22
22
 
23
23
  import {VersionsBar} from '../VersionsBar/VersionsBar';
24
24
  import {ClusterInfoSkeleton} from '../ClusterInfoSkeleton/ClusterInfoSkeleton';
@@ -126,7 +126,7 @@ export const ClusterInfo = ({
126
126
  }: ClusterInfoProps) => {
127
127
  const singleClusterMode = useTypedSelector((state) => state.singleClusterMode);
128
128
 
129
- const [clusterInfoHidden, setClusterInfoHidden] = useSetting(CLUSTER_INFO_HIDDEN_KEY, false);
129
+ const [clusterInfoHidden, setClusterInfoHidden] = useSetting<boolean>(CLUSTER_INFO_HIDDEN_KEY);
130
130
 
131
131
  const togleClusterInfoVisibility = () => {
132
132
  setClusterInfoHidden(!clusterInfoHidden);
@@ -141,7 +141,7 @@ export const ClusterInfo = ({
141
141
  const {info = [], links = []} = additionalClusterProps;
142
142
 
143
143
  const clusterInfo = getInfo(cluster, versionsValues, info, [
144
- {title: 'Internal Viewer', url: internalLink},
144
+ {title: DEVELOPER_UI, url: internalLink},
145
145
  ...links,
146
146
  ]);
147
147
 
@@ -1,3 +1,10 @@
1
+ import {Icon} from '@gravity-ui/uikit';
2
+ import cubes3Icon from '@gravity-ui/icons/svgs/cubes-3.svg';
3
+ import databasesIcon from '@gravity-ui/icons/svgs/databases.svg';
4
+ import hardDriveIcon from '@gravity-ui/icons/svgs/hard-drive.svg';
5
+
6
+ import versionsIcon from '../../assets/icons/versions.svg';
7
+
1
8
  import type {ValueOf} from '../../types/common';
2
9
  import routes, {createHref} from '../../routes';
3
10
 
@@ -13,18 +20,22 @@ export type ClusterTab = ValueOf<typeof clusterTabsIds>;
13
20
  const tenants = {
14
21
  id: clusterTabsIds.tenants,
15
22
  title: 'Databases',
23
+ icon: <Icon data={databasesIcon} />,
16
24
  };
17
25
  const nodes = {
18
26
  id: clusterTabsIds.nodes,
19
27
  title: 'Nodes',
28
+ icon: <Icon data={cubes3Icon} />,
20
29
  };
21
30
  const storage = {
22
31
  id: clusterTabsIds.storage,
23
32
  title: 'Storage',
33
+ icon: <Icon data={hardDriveIcon} />,
24
34
  };
25
35
  const versions = {
26
36
  id: clusterTabsIds.versions,
27
37
  title: 'Versions',
38
+ icon: <Icon data={versionsIcon} />,
28
39
  };
29
40
 
30
41
  export const clusterTabs = [tenants, nodes, storage, versions];
@@ -13,4 +13,13 @@
13
13
  border-bottom: 1px solid var(--yc-color-line-generic);
14
14
 
15
15
  @include body2-typography;
16
+
17
+ &__breadcrumb {
18
+ display: flex;
19
+ align-items: center;
20
+
21
+ &__icon {
22
+ margin-right: 3px;
23
+ }
24
+ }
16
25
  }
@@ -1,13 +1,19 @@
1
+ import {useEffect, useMemo} from 'react';
2
+ import {useHistory, useLocation} from 'react-router';
3
+ import {useDispatch} from 'react-redux';
1
4
  import block from 'bem-cn-lite';
2
- import {useHistory} from 'react-router';
3
5
 
4
- import {Breadcrumbs, BreadcrumbsItem} from '@gravity-ui/uikit';
6
+ import {Breadcrumbs, Icon} from '@gravity-ui/uikit';
5
7
 
6
8
  import {ExternalLinkWithIcon} from '../../components/ExternalLinkWithIcon/ExternalLinkWithIcon';
7
9
 
8
10
  import {backend, customBackend} from '../../store';
9
- import {HeaderItemType} from '../../store/reducers/header';
11
+ import {getClusterInfo} from '../../store/reducers/cluster/cluster';
10
12
  import {useTypedSelector} from '../../utils/hooks';
13
+ import {DEVELOPER_UI} from '../../utils/constants';
14
+ import {parseQuery} from '../../routes';
15
+
16
+ import {RawBreadcrumbItem, getBreadcrumbs} from './breadcrumbs';
11
17
 
12
18
  import './Header.scss';
13
19
 
@@ -21,23 +27,57 @@ const getInternalLink = (singleClusterMode: boolean) => {
21
27
  return backend + '/internal';
22
28
  };
23
29
 
24
- function Header() {
25
- const {singleClusterMode, header}: {singleClusterMode: boolean; header: HeaderItemType[]} =
26
- useTypedSelector((state) => state);
30
+ interface HeaderProps {
31
+ mainPage?: RawBreadcrumbItem;
32
+ }
27
33
 
34
+ function Header({mainPage}: HeaderProps) {
35
+ const dispatch = useDispatch();
28
36
  const history = useHistory();
37
+ const location = useLocation();
29
38
 
30
- const renderHeader = () => {
31
- const breadcrumbItems = header.reduce((acc, el) => {
39
+ const singleClusterMode = useTypedSelector((state) => state.singleClusterMode);
40
+ const {page, pageBreadcrumbsOptions} = useTypedSelector((state) => state.header);
41
+ const {data} = useTypedSelector((state) => state.cluster);
42
+
43
+ const queryParams = parseQuery(location);
44
+
45
+ const clusterNameFromQuery = queryParams.clusterName?.toString();
46
+
47
+ const clusterNameFinal = data?.Name || clusterNameFromQuery;
48
+
49
+ useEffect(() => {
50
+ dispatch(getClusterInfo(clusterNameFromQuery));
51
+ }, [dispatch, clusterNameFromQuery]);
52
+
53
+ const breadcrumbItems = useMemo(() => {
54
+ const rawBreadcrumbs: RawBreadcrumbItem[] = [];
55
+ let options = pageBreadcrumbsOptions;
56
+
57
+ if (mainPage) {
58
+ rawBreadcrumbs.push(mainPage);
59
+ }
60
+
61
+ if (clusterNameFinal) {
62
+ options = {
63
+ ...pageBreadcrumbsOptions,
64
+ clusterName: clusterNameFinal,
65
+ };
66
+ }
67
+
68
+ const breadcrumbs = getBreadcrumbs(page, options, rawBreadcrumbs, queryParams);
69
+
70
+ return breadcrumbs.map((item) => {
32
71
  const action = () => {
33
- if (el.link) {
34
- history.push(el.link);
72
+ if (item.link) {
73
+ history.push(item.link);
35
74
  }
36
75
  };
37
- acc.push({text: el.text, action});
38
- return acc;
39
- }, [] as BreadcrumbsItem[]);
76
+ return {...item, action};
77
+ });
78
+ }, [clusterNameFinal, mainPage, history, queryParams, page, pageBreadcrumbsOptions]);
40
79
 
80
+ const renderHeader = () => {
41
81
  return (
42
82
  <header className={b()}>
43
83
  <div>
@@ -45,11 +85,27 @@ function Header() {
45
85
  items={breadcrumbItems}
46
86
  lastDisplayedItemsCount={1}
47
87
  firstDisplayedItemsCount={1}
88
+ renderItemContent={({icon, text}) => {
89
+ if (!icon) {
90
+ return text;
91
+ }
92
+ return (
93
+ <span className={b('breadcrumb')}>
94
+ <Icon
95
+ width={16}
96
+ height={16}
97
+ data={icon}
98
+ className={b('breadcrumb__icon')}
99
+ />
100
+ {text}
101
+ </span>
102
+ );
103
+ }}
48
104
  />
49
105
  </div>
50
106
 
51
107
  <ExternalLinkWithIcon
52
- title={'Internal Viewer'}
108
+ title={DEVELOPER_UI}
53
109
  url={getInternalLink(singleClusterMode)}
54
110
  />
55
111
  </header>
@@ -0,0 +1,146 @@
1
+ import nodesRightIcon from '@gravity-ui/icons/svgs/nodes-right.svg';
2
+ import databaseIcon from '@gravity-ui/icons/svgs/database.svg';
3
+
4
+ import type {
5
+ BreadcrumbsOptions,
6
+ ClusterBreadcrumbsOptions,
7
+ NodeBreadcrumbsOptions,
8
+ Page,
9
+ TabletBreadcrumbsOptions,
10
+ TabletsBreadcrumbsOptions,
11
+ TenantBreadcrumbsOptions,
12
+ } from '../../store/reducers/header/types';
13
+ import routes, {createHref} from '../../routes';
14
+
15
+ import {getClusterPath} from '../Cluster/utils';
16
+ import {getTenantPath} from '../Tenant/TenantPages';
17
+ import {getDefaultNodePath} from '../Node/NodePages';
18
+
19
+ const prepareTenantName = (tenantName: string) => {
20
+ return tenantName.startsWith('/') ? tenantName.slice(1) : tenantName;
21
+ };
22
+
23
+ export interface RawBreadcrumbItem {
24
+ text: string;
25
+ link?: string;
26
+ icon?: SVGIconData;
27
+ }
28
+
29
+ const getClusterBreadcrumbs = (
30
+ options: ClusterBreadcrumbsOptions,
31
+ query = {},
32
+ ): RawBreadcrumbItem[] => {
33
+ const {clusterName, clusterTab} = options;
34
+
35
+ return [
36
+ {
37
+ text: clusterName || 'Cluster',
38
+ link: getClusterPath(clusterTab, query),
39
+ icon: nodesRightIcon,
40
+ },
41
+ ];
42
+ };
43
+
44
+ const getTenantBreadcrumbs = (
45
+ options: TenantBreadcrumbsOptions,
46
+ query = {},
47
+ ): RawBreadcrumbItem[] => {
48
+ const {tenantName} = options;
49
+
50
+ const text = tenantName ? prepareTenantName(tenantName) : 'Tenant';
51
+ const link = tenantName ? getTenantPath({...query, name: tenantName}) : undefined;
52
+
53
+ return [...getClusterBreadcrumbs(options, query), {text, link, icon: databaseIcon}];
54
+ };
55
+
56
+ const getNodeBreadcrumbs = (options: NodeBreadcrumbsOptions, query = {}): RawBreadcrumbItem[] => {
57
+ const {tenantName, nodeId} = options;
58
+
59
+ let breadcrumbs: RawBreadcrumbItem[];
60
+
61
+ // Compute nodes have tenantName, storage nodes doesn't
62
+ const isStorageNode = !tenantName;
63
+
64
+ if (isStorageNode) {
65
+ breadcrumbs = getClusterBreadcrumbs(options, query);
66
+ } else {
67
+ breadcrumbs = getTenantBreadcrumbs(options, query);
68
+ }
69
+
70
+ const text = nodeId ? `Node ${nodeId}` : 'Node';
71
+ const link = nodeId ? getDefaultNodePath(nodeId, query) : undefined;
72
+
73
+ breadcrumbs.push({text, link});
74
+
75
+ return breadcrumbs;
76
+ };
77
+
78
+ const getTabletsBreadcrubms = (
79
+ options: TabletsBreadcrumbsOptions,
80
+ query = {},
81
+ ): RawBreadcrumbItem[] => {
82
+ const {tenantName, nodeIds, state, type} = options;
83
+
84
+ let breadcrumbs: RawBreadcrumbItem[];
85
+
86
+ // Cluster system tablets don't have tenantName
87
+ if (tenantName) {
88
+ breadcrumbs = getTenantBreadcrumbs(options, query);
89
+ } else {
90
+ breadcrumbs = getClusterBreadcrumbs(options, query);
91
+ }
92
+
93
+ const link = createHref(routes.tabletsFilters, undefined, {
94
+ nodeIds,
95
+ state,
96
+ type,
97
+ path: tenantName,
98
+ });
99
+
100
+ breadcrumbs.push({text: 'Tablets', link});
101
+
102
+ return breadcrumbs;
103
+ };
104
+
105
+ const getTabletBreadcrubms = (
106
+ options: TabletBreadcrumbsOptions,
107
+ query = {},
108
+ ): RawBreadcrumbItem[] => {
109
+ const {tabletId} = options;
110
+
111
+ const breadcrumbs = getTabletsBreadcrubms(options, query);
112
+
113
+ breadcrumbs.push({
114
+ text: tabletId || 'Tablet',
115
+ });
116
+
117
+ return breadcrumbs;
118
+ };
119
+
120
+ export const getBreadcrumbs = (
121
+ page: Page,
122
+ options: BreadcrumbsOptions,
123
+ rawBreadcrumbs: RawBreadcrumbItem[] = [],
124
+ query = {},
125
+ ) => {
126
+ switch (page) {
127
+ case 'cluster': {
128
+ return [...rawBreadcrumbs, ...getClusterBreadcrumbs(options, query)];
129
+ }
130
+ case 'tenant': {
131
+ return [...rawBreadcrumbs, ...getTenantBreadcrumbs(options, query)];
132
+ }
133
+ case 'node': {
134
+ return [...rawBreadcrumbs, ...getNodeBreadcrumbs(options, query)];
135
+ }
136
+ case 'tablets': {
137
+ return [...rawBreadcrumbs, ...getTabletsBreadcrubms(options, query)];
138
+ }
139
+ case 'tablet': {
140
+ return [...rawBreadcrumbs, ...getTabletBreadcrubms(options, query)];
141
+ }
142
+ default: {
143
+ return rawBreadcrumbs;
144
+ }
145
+ }
146
+ };
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
- import {useRouteMatch} from 'react-router';
2
+ import {useLocation, useRouteMatch} from 'react-router';
3
3
  import cn from 'bem-cn-lite';
4
- import {useDispatch, useSelector} from 'react-redux';
4
+ import {useDispatch} from 'react-redux';
5
5
  import _ from 'lodash';
6
6
 
7
7
  import {Tabs} from '@gravity-ui/uikit';
@@ -16,10 +16,12 @@ import {Loader} from '../../components/Loader';
16
16
  import {BasicNodeViewer} from '../../components/BasicNodeViewer';
17
17
 
18
18
  import {getNodeInfo, resetNode} from '../../store/reducers/node/node';
19
- import routes, {createHref} from '../../routes';
20
- import {HeaderItemType, setHeader} from '../../store/reducers/header';
19
+ import routes, {createHref, parseQuery} from '../../routes';
20
+ import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
21
21
  import {AutoFetcher} from '../../utils/autofetcher';
22
- import {clusterTabsIds, getClusterPath} from '../Cluster/utils';
22
+ import {useTypedSelector} from '../../utils/hooks';
23
+
24
+ import {clusterTabsIds} from '../Cluster/utils';
23
25
 
24
26
  import './Node.scss';
25
27
 
@@ -36,19 +38,16 @@ interface NodeProps {
36
38
 
37
39
  function Node(props: NodeProps) {
38
40
  const dispatch = useDispatch();
41
+ const location = useLocation();
39
42
 
40
- const wasLoaded = useSelector((state: any) => state.node.wasLoaded);
41
- const loading = useSelector((state: any) => state.node.loading);
42
- const error = useSelector((state: any) => state.node.error);
43
-
44
- const node = useSelector((state: any) => state.node?.data?.SystemStateInfo?.[0]);
45
-
46
- const nodeHost = node?.Host;
43
+ const {loading, wasLoaded, error, data} = useTypedSelector((state) => state.node);
44
+ const node = data?.SystemStateInfo?.[0];
47
45
 
48
46
  const match =
49
47
  useRouteMatch<{id: string; activeTab: string}>(routes.node) ?? Object.create(null);
50
48
 
51
49
  const {id: nodeId, activeTab} = match.params;
50
+ const {tenantName: tenantNameFromQuery} = parseQuery(location);
52
51
 
53
52
  const {activeTabVerified, nodeTabs} = React.useMemo(() => {
54
53
  const hasStorage = _.find(node?.Roles as any[], (el) => el === STORAGE_ROLE);
@@ -69,21 +68,16 @@ function Node(props: NodeProps) {
69
68
  }, [activeTab, node]);
70
69
 
71
70
  React.useEffect(() => {
72
- const headerItems: HeaderItemType[] = [
73
- {
74
- text: 'Cluster',
75
- link: getClusterPath(clusterTabsIds.nodes),
76
- },
77
- ];
78
-
79
- if (nodeHost) {
80
- headerItems.push({
81
- text: nodeHost,
82
- });
83
- }
84
-
85
- dispatch(setHeader(headerItems));
86
- }, [dispatch, nodeHost]);
71
+ const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString();
72
+
73
+ dispatch(
74
+ setHeaderBreadcrumbs('node', {
75
+ clusterTab: clusterTabsIds.nodes,
76
+ tenantName,
77
+ nodeId,
78
+ }),
79
+ );
80
+ }, [dispatch, node, nodeId, tenantNameFromQuery]);
87
81
 
88
82
  React.useEffect(() => {
89
83
  const fetchData = () => dispatch(getNodeInfo(nodeId));