diff --git a/src/components/BasicNodeViewer/BasicNodeViewer.scss b/src/components/BasicNodeViewer/BasicNodeViewer.scss
deleted file mode 100644
index d6b3580074..0000000000
--- a/src/components/BasicNodeViewer/BasicNodeViewer.scss
+++ /dev/null
@@ -1,35 +0,0 @@
-@use '../../styles/mixins.scss';
-
-.basic-node-viewer {
- display: flex;
- align-items: center;
-
- margin: 15px 0;
-
- @include mixins.body-2-typography();
-
- &__title {
- margin: 0 20px 0 0;
-
- font-weight: 600;
- text-transform: uppercase;
- }
-
- &__id {
- margin: 0 15px 0 24px;
- }
-
- &__label {
- margin-right: 10px;
-
- line-height: 18px;
- white-space: nowrap;
-
- color: var(--g-color-text-hint);
- }
-
- &__link {
- margin-left: 5px;
- @extend .link;
- }
-}
diff --git a/src/components/BasicNodeViewer/BasicNodeViewer.tsx b/src/components/BasicNodeViewer/BasicNodeViewer.tsx
deleted file mode 100644
index 07eb9fcacb..0000000000
--- a/src/components/BasicNodeViewer/BasicNodeViewer.tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import React from 'react';
-
-import {ArrowUpRightFromSquare} from '@gravity-ui/icons';
-import {Icon} from '@gravity-ui/uikit';
-
-import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
-import type {PreparedNode} from '../../store/reducers/node/types';
-import type {AdditionalNodesProps} from '../../types/additionalProps';
-import {cn} from '../../utils/cn';
-import {
- createDeveloperUIInternalPageHref,
- createDeveloperUILinkWithNodeId,
-} from '../../utils/developerUI/developerUI';
-import {useTypedSelector} from '../../utils/hooks';
-import {EntityStatus} from '../EntityStatus/EntityStatus';
-import {Tags} from '../Tags';
-
-import './BasicNodeViewer.scss';
-
-const b = cn('basic-node-viewer');
-
-interface BasicNodeViewerProps {
- node: PreparedNode;
- additionalNodesProps?: AdditionalNodesProps;
- className?: string;
-}
-
-export const BasicNodeViewer = ({node, additionalNodesProps, className}: BasicNodeViewerProps) => {
- const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
-
- let developerUIInternalHref: string | undefined;
-
- if (additionalNodesProps?.getNodeRef) {
- const developerUIHref = additionalNodesProps.getNodeRef(node);
- developerUIInternalHref = developerUIHref
- ? createDeveloperUIInternalPageHref(developerUIHref)
- : undefined;
- } else if (node.NodeId) {
- const developerUIHref = createDeveloperUILinkWithNodeId(node.NodeId);
- developerUIInternalHref = createDeveloperUIInternalPageHref(developerUIHref);
- }
-
- return (
-
{entityName}
diff --git a/src/components/FullNodeViewer/FullNodeViewer.scss b/src/components/FullNodeViewer/FullNodeViewer.scss
index 4a6f449147..58f3a6b813 100644
--- a/src/components/FullNodeViewer/FullNodeViewer.scss
+++ b/src/components/FullNodeViewer/FullNodeViewer.scss
@@ -3,15 +3,13 @@
.full-node-viewer {
@include mixins.body-2-typography();
- &__common-info {
+ &__section {
display: flex;
flex-direction: column;
- justify-content: flex-start;
- align-items: stretch;
- }
- &__section {
- border-radius: 10px;
+ width: max-content;
+ min-width: 300px;
+ max-width: 500px;
&_pools {
display: grid;
@@ -26,8 +24,10 @@
}
&__section-title {
- margin: 15px 0 10px;
+ @include mixins.info-viewer-title();
+ }
- font-weight: 600;
+ &__role {
+ color: var(--g-color-text-secondary);
}
}
diff --git a/src/components/FullNodeViewer/FullNodeViewer.tsx b/src/components/FullNodeViewer/FullNodeViewer.tsx
index ae8d3a07f9..2251740f42 100644
--- a/src/components/FullNodeViewer/FullNodeViewer.tsx
+++ b/src/components/FullNodeViewer/FullNodeViewer.tsx
@@ -1,57 +1,96 @@
+import {Flex} from '@gravity-ui/uikit';
+
import type {PreparedNode} from '../../store/reducers/node/types';
import {cn} from '../../utils/cn';
-import {LOAD_AVERAGE_TIME_INTERVALS} from '../../utils/constants';
+import {useNodeDeveloperUIHref} from '../../utils/hooks/useNodeDeveloperUIHref';
import {InfoViewer} from '../InfoViewer/InfoViewer';
import type {InfoViewerItem} from '../InfoViewer/InfoViewer';
+import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon';
import {PoolUsage} from '../PoolUsage/PoolUsage';
import {ProgressViewer} from '../ProgressViewer/ProgressViewer';
import {NodeUptime} from '../UptimeViewer/UptimeViewer';
+import i18n from './i18n';
+
import './FullNodeViewer.scss';
const b = cn('full-node-viewer');
interface FullNodeViewerProps {
- node: PreparedNode | undefined;
+ node?: PreparedNode;
className?: string;
}
+const getLoadAverageIntervalTitle = (index: number) => {
+ return [i18n('la-interval-1m'), i18n('la-interval-5m'), i18n('la-interval-15m')][index];
+};
export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => {
- const endpointsInfo = node?.Endpoints?.map(({Name, Address}) => ({
- label: Name,
- value: Address,
- }));
+ const developerUIHref = useNodeDeveloperUIHref(node);
const commonInfo: InfoViewerItem[] = [];
- // Do not add DB field for static nodes (they have no Tenants)
if (node?.Tenants?.length) {
- commonInfo.push({label: 'Database', value: node.Tenants[0]});
+ commonInfo.push({label: i18n('database'), value: node.Tenants[0]});
}
commonInfo.push(
- {label: 'Version', value: node?.Version},
+ {label: i18n('version'), value: node?.Version},
{
- label: 'Uptime',
+ label: i18n('uptime'),
value:
,
},
- {label: 'DC', value: node?.DataCenterDescription || node?.DC},
- {label: 'Rack', value: node?.Rack},
+ {label: i18n('dc'), value: node?.DataCenterDescription || node?.DC},
);
+ if (node?.Rack) {
+ commonInfo.push({label: i18n('rack'), value: node?.Rack});
+ }
+
+ if (developerUIHref) {
+ commonInfo.push({
+ label: i18n('links'),
+ value:
,
+ });
+ }
+
+ const endpointsInfo = node?.Endpoints?.map(({Name, Address}) => ({
+ label: Name,
+ value: Address,
+ }));
+
const averageInfo = node?.LoadAveragePercents?.map((load, loadIndex) => ({
- label: LOAD_AVERAGE_TIME_INTERVALS[loadIndex],
+ label: getLoadAverageIntervalTitle(loadIndex),
value: (
),
}));
+ if (!node) {
+ return
{i18n('no-data')}
;
+ }
+
return (
-
- {node ? (
-
+
+
+
+
+
+ {endpointsInfo && endpointsInfo.length ? (
+
+ ) : null}
+
+
+
-
Pools
+
{i18n('title.pools')}
{node?.PoolStats?.map((pool, poolIndex) => (
@@ -59,25 +98,26 @@ export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => {
- {endpointsInfo && endpointsInfo.length && (
-
- )}
-
-
-
-
- ) : (
-
no data
- )}
+
+
+ {node.Roles && node.Roles.length ? (
+
+
+
{i18n('title.roles')}
+ {node?.Roles?.map((role) => (
+
+ {role}
+
+ ))}
+
+
+ ) : null}
+
);
};
diff --git a/src/components/FullNodeViewer/i18n/en.json b/src/components/FullNodeViewer/i18n/en.json
new file mode 100644
index 0000000000..d20f622645
--- /dev/null
+++ b/src/components/FullNodeViewer/i18n/en.json
@@ -0,0 +1,21 @@
+{
+ "database": "Database",
+ "uptime": "Uptime",
+ "version": "Version",
+ "dc": "DC",
+ "rack": "Rack",
+ "links": "Links",
+
+ "la-interval-1m": "1 min",
+ "la-interval-5m": "5 min",
+ "la-interval-15m": "15 min",
+
+ "developer-ui": "Developer UI",
+ "no-data": "No data",
+
+ "title.common-info": "Common info",
+ "title.endpoints": "Endpoints",
+ "title.roles": "Roles",
+ "title.pools": "Pools",
+ "title.load-average": "Load average"
+}
diff --git a/src/components/FullNodeViewer/i18n/index.ts b/src/components/FullNodeViewer/i18n/index.ts
new file mode 100644
index 0000000000..bd139eb7c8
--- /dev/null
+++ b/src/components/FullNodeViewer/i18n/index.ts
@@ -0,0 +1,7 @@
+import {registerKeysets} from '../../../utils/i18n';
+
+import en from './en.json';
+
+const COMPONENT = 'ydb-node-info';
+
+export default registerKeysets(COMPONENT, {en});
diff --git a/src/components/NodeHostWrapper/NodeHostWrapper.tsx b/src/components/NodeHostWrapper/NodeHostWrapper.tsx
index a17960272e..2a9e18d0db 100644
--- a/src/components/NodeHostWrapper/NodeHostWrapper.tsx
+++ b/src/components/NodeHostWrapper/NodeHostWrapper.tsx
@@ -54,9 +54,13 @@ export const NodeHostWrapper = ({
}
const nodePath = isNodeAvailable
- ? getDefaultNodePath(node.NodeId, {
- database: database ?? node.TenantName,
- })
+ ? getDefaultNodePath(
+ node.NodeId,
+ {
+ database: database ?? node.TenantName,
+ },
+ node.TenantName ? 'tablets' : 'storage',
+ )
: undefined;
return (
diff --git a/src/components/PageMeta/PageMeta.tsx b/src/components/PageMeta/PageMeta.tsx
index 4dd5a74859..c894e265f5 100644
--- a/src/components/PageMeta/PageMeta.tsx
+++ b/src/components/PageMeta/PageMeta.tsx
@@ -14,13 +14,15 @@ interface PageMetaProps {
loading?: boolean;
}
+const separator = '\u00a0\u00a0\u00B7\u00a0\u00a0';
+
export function PageMeta({items, loading}: PageMetaProps) {
const renderContent = () => {
if (loading) {
return
;
}
- return items.filter((item) => Boolean(item)).join('\u00a0\u00a0\u00B7\u00a0\u00a0');
+ return items.filter((item) => Boolean(item)).join(separator);
};
return
{renderContent()}
;
diff --git a/src/components/TabletsStatistic/TabletsStatistic.tsx b/src/components/TabletsStatistic/TabletsStatistic.tsx
index 750cb02005..6515a91150 100644
--- a/src/components/TabletsStatistic/TabletsStatistic.tsx
+++ b/src/components/TabletsStatistic/TabletsStatistic.tsx
@@ -1,6 +1,6 @@
import {Link} from 'react-router-dom';
-import {TABLETS, getDefaultNodePath} from '../../containers/Node/NodePages';
+import {getDefaultNodePath} from '../../containers/Node/NodePages';
import type {TTabletStateInfo} from '../../types/api/tablet';
import {cn} from '../../utils/cn';
import {getTabletLabel} from '../../utils/constants';
@@ -31,7 +31,7 @@ interface TabletsStatisticProps {
export const TabletsStatistic = ({tablets = [], database, nodeId}: TabletsStatisticProps) => {
const renderTabletInfo = (item: ReturnType
[number], index: number) => {
- const tabletsPath = getDefaultNodePath(nodeId, {database}, TABLETS);
+ const tabletsPath = getDefaultNodePath(nodeId, {database}, 'tablets');
const label = `${item.label}: ${item.count}`;
const className = b('tablet', {state: item.state?.toLowerCase()});
diff --git a/src/components/VDisk/utils.ts b/src/components/VDisk/utils.ts
index da81251c4e..f738878b81 100644
--- a/src/components/VDisk/utils.ts
+++ b/src/components/VDisk/utils.ts
@@ -1,5 +1,5 @@
-import {STRUCTURE} from '../../containers/Node/NodePages';
-import routes, {createHref, getVDiskPagePath} from '../../routes';
+import {getDefaultNodePath} from '../../containers/Node/NodePages';
+import {getVDiskPagePath} from '../../routes';
import type {TVDiskStateInfo, TVSlotId} from '../../types/api/vdisk';
import {valueIsDefined} from '../../utils';
import {stringifyVdiskId} from '../../utils/dataFormatters/dataFormatters';
@@ -18,13 +18,13 @@ export function getVDiskLink(data: TVDiskStateInfo | TVSlotId) {
) {
vDiskPath = getVDiskPagePath(VDiskSlotId, data.PDiskId, data.NodeId);
} else if (valueIsDefined(data.NodeId) && isFullVDiskData(data)) {
- vDiskPath = createHref(
- routes.node,
- {id: data.NodeId, activeTab: STRUCTURE},
+ vDiskPath = getDefaultNodePath(
+ data.NodeId,
{
- pdiskId: data.PDiskId,
+ pdiskId: data.PDiskId?.toString(),
vdiskId: stringifyVdiskId(data.VDiskId),
},
+ 'structure',
);
}
diff --git a/src/containers/AppWithClusters/AppWithClusters.tsx b/src/containers/AppWithClusters/AppWithClusters.tsx
index 623e640564..3dcafb028d 100644
--- a/src/containers/AppWithClusters/AppWithClusters.tsx
+++ b/src/containers/AppWithClusters/AppWithClusters.tsx
@@ -12,7 +12,6 @@ import {App, AppSlots} from '../App';
import type {YDBEmbeddedUISettings} from '../UserSettings/settings';
import {ExtendedCluster} from './ExtendedCluster/ExtendedCluster';
-import {ExtendedNode} from './ExtendedNode/ExtendedNode';
import {ExtendedTenant} from './ExtendedTenant/ExtendedTenant';
export interface AppWithClustersProps {
@@ -45,11 +44,6 @@ export function AppWithClusters({
);
}}
-
- {({component}) => {
- return ;
- }}
-
{({component}) => {
return (
diff --git a/src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx b/src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx
deleted file mode 100644
index dbe55137b5..0000000000
--- a/src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import {useClusterBaseInfo} from '../../../store/reducers/cluster/cluster';
-import type {Node} from '../../Node/Node';
-import {useAdditionalNodeProps} from '../useClusterData';
-
-export function ExtendedNode({component: NodeComponent}: {component: typeof Node}) {
- const {balancer} = useClusterBaseInfo();
- const {additionalNodesProps} = useAdditionalNodeProps({balancer});
-
- return ;
-}
diff --git a/src/containers/Header/breadcrumbs.tsx b/src/containers/Header/breadcrumbs.tsx
index 14e036b499..3163f71edd 100644
--- a/src/containers/Header/breadcrumbs.tsx
+++ b/src/containers/Header/breadcrumbs.tsx
@@ -25,7 +25,7 @@ import {
} from '../../store/reducers/tenant/constants';
import {CLUSTER_DEFAULT_TITLE, getTabletLabel} from '../../utils/constants';
import {getClusterPath} from '../Cluster/utils';
-import {TABLETS, getDefaultNodePath} from '../Node/NodePages';
+import {getDefaultNodePath} from '../Node/NodePages';
import {TenantTabsGroups, getTenantPath} from '../Tenant/TenantPages';
import {headerKeyset} from './i18n';
@@ -78,7 +78,7 @@ const getTenantBreadcrumbs: GetBreadcrumbs = (options,
const getNodeBreadcrumbs: GetBreadcrumbs = (options, query = {}) => {
const {nodeId, nodeRole, nodeActiveTab, tenantName} = options;
- const tenantQuery = getQueryForTenant(nodeActiveTab === TABLETS ? 'tablets' : 'nodes');
+ const tenantQuery = getQueryForTenant(nodeActiveTab === 'tablets' ? 'tablets' : 'nodes');
const breadcrumbs = tenantName
? getTenantBreadcrumbs(options, {...query, ...tenantQuery})
diff --git a/src/containers/Node/Node.scss b/src/containers/Node/Node.scss
index 86bf17c0b6..d0db7fa48a 100644
--- a/src/containers/Node/Node.scss
+++ b/src/containers/Node/Node.scss
@@ -1,60 +1,34 @@
@use '../../styles/mixins';
.node {
- overflow: auto;
- @include mixins.flex-container();
-
- &__header {
- margin: 16px 20px;
- }
+ position: relative;
- &__content {
- position: relative;
-
- overflow: auto;
- @include mixins.flex-container();
- }
-
- &__storage {
- overflow: auto;
+ overflow: auto;
- height: 100%;
- padding: 0 20px;
- }
+ height: 100%;
+ padding: 0 20px;
+ &__meta,
+ &__title,
+ &__info,
+ &__error,
&__tabs {
- display: flex;
- justify-content: space-between;
- align-items: center;
-
- padding: 0 20px;
- @include mixins.tabs-wrapper-styles();
- }
-
- &__tab {
- margin-right: 40px;
-
- text-decoration: none;
-
- &:last-child {
- margin-right: 0;
- }
+ position: sticky;
+ left: 0;
- &:first-letter {
- text-transform: uppercase;
- }
+ margin-bottom: 20px;
}
- &__overview-wrapper {
- padding: 0 20px 20px;
+ &__meta {
+ margin-top: 20px;
}
- &__node-page-wrapper {
- height: 100%;
- padding: 20px;
+ &__tabs,
+ &__error {
+ margin-bottom: 0;
}
- &__error {
- padding: 0 20px;
+ &__tabs {
+ @include mixins.tabs-wrapper-styles();
}
}
diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx
index 5fe6d56b03..778e19c301 100644
--- a/src/containers/Node/Node.tsx
+++ b/src/containers/Node/Node.tsx
@@ -1,15 +1,18 @@
import React from 'react';
+import type {TabsItemProps} from '@gravity-ui/uikit';
import {Tabs} from '@gravity-ui/uikit';
+import {skipToken} from '@reduxjs/toolkit/query';
import {Helmet} from 'react-helmet-async';
-import {Link, useRouteMatch} from 'react-router-dom';
+import {useRouteMatch} from 'react-router-dom';
import {useQueryParams} from 'use-query-params';
-import {AutoRefreshControl} from '../../components/AutoRefreshControl/AutoRefreshControl';
-import {BasicNodeViewer} from '../../components/BasicNodeViewer';
+import {EntityPageTitle} from '../../components/EntityPageTitle/EntityPageTitle';
import {ResponseError} from '../../components/Errors/ResponseError';
import {FullNodeViewer} from '../../components/FullNodeViewer/FullNodeViewer';
-import {Loader} from '../../components/Loader';
+import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewerSkeleton';
+import {InternalLink} from '../../components/InternalLink';
+import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta';
import routes from '../../routes';
import {
useCapabilitiesLoaded,
@@ -17,23 +20,16 @@ import {
} from '../../store/reducers/capabilities/hooks';
import {setHeaderBreadcrumbs} from '../../store/reducers/header/header';
import {nodeApi} from '../../store/reducers/node/node';
-import type {AdditionalNodesProps} from '../../types/additionalProps';
+import type {PreparedNode} from '../../store/reducers/node/types';
import {cn} from '../../utils/cn';
import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks';
import {PaginatedStorage} from '../Storage/PaginatedStorage';
import {Tablets} from '../Tablets';
import type {NodeTab} from './NodePages';
-import {
- NODE_PAGES,
- OVERVIEW,
- STORAGE,
- STRUCTURE,
- TABLETS,
- getDefaultNodePath,
- nodePageQueryParams,
-} from './NodePages';
+import {NODE_TABS, getDefaultNodePath, nodePageQueryParams, nodePageTabSchema} from './NodePages';
import NodeStructure from './NodeStructure/NodeStructure';
+import i18n from './i18n';
import './Node.scss';
@@ -41,132 +37,208 @@ const b = cn('node');
const STORAGE_ROLE = 'Storage';
-interface NodeProps {
- additionalNodesProps?: AdditionalNodesProps;
- className?: string;
-}
-
-export function Node(props: NodeProps) {
+export function Node() {
const container = React.useRef(null);
const dispatch = useTypedDispatch();
- const match =
- useRouteMatch<{id: string; activeTab: string}>(routes.node) ?? Object.create(null);
+ const match = useRouteMatch<{id: string; activeTab: string}>(routes.node);
+
+ const nodeId = match?.params.id;
+ const activeTabIdFromQuery = match?.params.activeTab;
- const {id: nodeId, activeTab} = match.params;
const [{database: tenantNameFromQuery}] = useQueryParams(nodePageQueryParams);
+ const activeTabId = nodePageTabSchema.parse(activeTabIdFromQuery);
+
const [autoRefreshInterval] = useAutoRefreshInterval();
- const {currentData, isFetching, error} = nodeApi.useGetNodeInfoQuery(
- {nodeId},
- {pollingInterval: autoRefreshInterval},
- );
- const loading = isFetching && currentData === undefined;
- const node = currentData;
+
+ const params = nodeId ? {nodeId} : skipToken;
+ const {
+ currentData: node,
+ isLoading,
+ error,
+ } = nodeApi.useGetNodeInfoQuery(params, {pollingInterval: autoRefreshInterval});
+
const capabilitiesLoaded = useCapabilitiesLoaded();
const isDiskPagesAvailable = useDiskPagesAvailable();
- const {activeTabVerified, nodeTabs} = React.useMemo(() => {
- const hasStorage = node?.Roles?.find((el) => el === STORAGE_ROLE);
+ const pageLoading = isLoading || !capabilitiesLoaded;
- let nodePages = hasStorage ? NODE_PAGES : NODE_PAGES.filter((el) => el.id !== STORAGE);
+ const isStorageNode = node?.Roles?.find((el) => el === STORAGE_ROLE);
+
+ const {activeTab, nodeTabs} = React.useMemo(() => {
+ let actulaNodeTabs = isStorageNode
+ ? NODE_TABS
+ : NODE_TABS.filter((el) => el.id !== 'storage');
if (isDiskPagesAvailable) {
- nodePages = nodePages.filter((el) => el.id !== STRUCTURE);
+ actulaNodeTabs = actulaNodeTabs.filter((el) => el.id !== 'structure');
}
- const actualNodeTabs = nodePages.map((page) => {
- return {
- ...page,
- title: page.name,
- };
- });
+ const actualActiveTab =
+ actulaNodeTabs.find(({id}) => id === activeTabId) ?? actulaNodeTabs[0];
+
+ return {activeTab: actualActiveTab, nodeTabs: actulaNodeTabs};
+ }, [isStorageNode, isDiskPagesAvailable, activeTabId]);
+
+ const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString();
- let actualActiveTab = actualNodeTabs.find(({id}) => id === activeTab);
- if (!actualActiveTab) {
- actualActiveTab = actualNodeTabs[0];
+ React.useEffect(() => {
+ // Dispatch only if loaded to get correct node role
+ if (!isLoading) {
+ dispatch(
+ setHeaderBreadcrumbs('node', {
+ tenantName,
+ nodeRole: isStorageNode ? 'Storage' : 'Compute',
+ nodeId,
+ }),
+ );
}
+ }, [dispatch, tenantName, nodeId, isLoading, isStorageNode]);
+
+ return (
+
+ {}
+ {}
+ {}
+ {error ? : null}
+ {}
+ {nodeId ? (
+
+ ) : null}
+
+ );
+}
- return {activeTabVerified: actualActiveTab, nodeTabs: actualNodeTabs};
- }, [activeTab, node, isDiskPagesAvailable]);
+interface NodePageHelmetProps {
+ node?: PreparedNode;
+ activeTabTitle?: string;
+}
- const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString();
+function NodePageHelmet({node, activeTabTitle}: NodePageHelmetProps) {
+ const host = node?.Host ? node.Host : i18n('node');
+ return (
+
+ {activeTabTitle}
+
+ );
+}
+
+interface NodePageMetaProps {
+ node?: PreparedNode;
+ loading?: boolean;
+}
- let nodeRole: 'Storage' | 'Compute' | undefined;
- if (node) {
- // Compute nodes have tenantName, storage nodes doesn't
- const isStorage = !node?.Tenants?.[0];
- nodeRole = isStorage ? 'Storage' : 'Compute';
+function NodePageMeta({node, loading}: NodePageMetaProps) {
+ const hostItem = node?.Host ? `${i18n('fqdn')}: ${node.Host}` : undefined;
+ const dcItem = node?.DC ? `${i18n('dc')}: ${node.DC}` : undefined;
+
+ return (
+
+ );
+}
+
+interface NodePageTitleProps {
+ node?: PreparedNode;
+}
+
+function NodePageTitle({node}: NodePageTitleProps) {
+ return (
+
+ );
+}
+
+interface NodePageInfoProps {
+ node?: PreparedNode;
+ loading?: boolean;
+}
+
+function NodePageInfo({node, loading}: NodePageInfoProps) {
+ if (loading) {
+ return ;
}
- React.useEffect(() => {
- dispatch(
- setHeaderBreadcrumbs('node', {
- tenantName,
- nodeRole,
- nodeId,
- }),
- );
- }, [dispatch, tenantName, nodeId, nodeRole]);
+ return ;
+}
+interface NodePageContentProps {
+ nodeId: string;
+ tenantName?: string;
+
+ activeTabId: NodeTab;
+ tabs: TabsItemProps[];
+
+ parentContainer: React.RefObject;
+}
+
+function NodePageContent({
+ nodeId,
+ tenantName,
+ activeTabId,
+ tabs,
+ parentContainer,
+}: NodePageContentProps) {
const renderTabs = () => {
return (
(
-
- {tabNode}
-
- )}
- allowNotSelected={true}
+ items={tabs}
+ activeTab={activeTabId}
+ wrapTo={({id}, tabNode) => {
+ const path = getDefaultNodePath(
+ nodeId,
+ {database: tenantName},
+ id as NodeTab,
+ );
+ return (
+
+ {tabNode}
+
+ );
+ }}
/>
-
);
};
+
const renderTabContent = () => {
- switch (activeTabVerified.id) {
- case STORAGE: {
- return (
-
- );
- }
- case TABLETS: {
+ switch (activeTabId) {
+ case 'storage': {
return (
-
);
}
-
- case STRUCTURE: {
- return ;
+ case 'tablets': {
+ return ;
}
- case OVERVIEW: {
- return ;
+
+ case 'structure': {
+ return ;
}
default:
@@ -174,35 +246,10 @@ export function Node(props: NodeProps) {
}
};
- if (loading || !capabilitiesLoaded) {
- return ;
- }
-
- if (node) {
- return (
-
-
- {activeTabVerified.title}
-
-
-
- {error ?
: null}
- {renderTabs()}
-
{renderTabContent()}
-
- );
- }
-
- if (error) {
- return ;
- }
-
- return no node data
;
+ return (
+
+ {renderTabs()}
+ {renderTabContent()}
+
+ );
}
diff --git a/src/containers/Node/NodePages.ts b/src/containers/Node/NodePages.ts
index 7c46c6ed09..49ade751f1 100644
--- a/src/containers/Node/NodePages.ts
+++ b/src/containers/Node/NodePages.ts
@@ -1,36 +1,47 @@
import {StringParam} from 'use-query-params';
+import {z} from 'zod';
import type {QueryParamsTypeFromQueryObject} from '../../routes';
import routes, {createHref} from '../../routes';
+import type {ValueOf} from '../../types/common';
-export const STORAGE = 'storage';
-export const TABLETS = 'tablets';
-export const OVERVIEW = 'overview';
-export const STRUCTURE = 'structure';
+import i18n from './i18n';
-export type NodeTab = typeof OVERVIEW | typeof STORAGE | typeof STRUCTURE | typeof TABLETS;
+const NODE_TABS_IDS = {
+ storage: 'storage',
+ tablets: 'tablets',
+ structure: 'structure',
+} as const;
-export const NODE_PAGES = [
- {
- id: OVERVIEW,
- name: 'Overview',
- },
+export type NodeTab = ValueOf;
+
+export const NODE_TABS = [
{
- id: STORAGE,
- name: 'Storage',
+ id: NODE_TABS_IDS.storage,
+ get title() {
+ return i18n('tabs.storage');
+ },
},
{
- id: STRUCTURE,
- name: 'Structure',
+ id: NODE_TABS_IDS.structure,
+ get title() {
+ return i18n('tabs.structure');
+ },
},
{
- id: TABLETS,
- name: 'Tablets',
+ id: NODE_TABS_IDS.tablets,
+ get title() {
+ return i18n('tabs.tablets');
+ },
},
];
+export const nodePageTabSchema = z.nativeEnum(NODE_TABS_IDS).catch(NODE_TABS_IDS.tablets);
+
export const nodePageQueryParams = {
database: StringParam,
+ pdiskId: StringParam,
+ vdiskId: StringParam,
};
type NodePageQuery = QueryParamsTypeFromQueryObject;
@@ -38,7 +49,7 @@ type NodePageQuery = QueryParamsTypeFromQueryObject;
export function getDefaultNodePath(
nodeId: string | number,
query: NodePageQuery = {},
- activeTab: NodeTab = OVERVIEW,
+ activeTab?: NodeTab,
) {
return createHref(
routes.node,
diff --git a/src/containers/Node/i18n/en.json b/src/containers/Node/i18n/en.json
index dd9f9a9291..6fe9179f88 100644
--- a/src/containers/Node/i18n/en.json
+++ b/src/containers/Node/i18n/en.json
@@ -1,4 +1,12 @@
{
"pdisk.developer-ui-button-title": "PDisk Developer UI page",
- "vdisk.developer-ui-button-title": "VDisk Developer UI page"
+ "vdisk.developer-ui-button-title": "VDisk Developer UI page",
+
+ "tabs.storage": "Storage",
+ "tabs.structure": "Structure",
+ "tabs.tablets": "Tablets",
+
+ "node": "Node",
+ "fqdn": "FQDN",
+ "dc": "DC"
}
diff --git a/src/containers/Node/i18n/index.ts b/src/containers/Node/i18n/index.ts
index 8a94645ebc..554a1efd2b 100644
--- a/src/containers/Node/i18n/index.ts
+++ b/src/containers/Node/i18n/index.ts
@@ -1,8 +1,7 @@
import {registerKeysets} from '../../../utils/i18n';
import en from './en.json';
-import ru from './ru.json';
const COMPONENT = 'ydb-node-page';
-export default registerKeysets(COMPONENT, {en, ru});
+export default registerKeysets(COMPONENT, {en});
diff --git a/src/containers/Node/i18n/ru.json b/src/containers/Node/i18n/ru.json
deleted file mode 100644
index a59adce0f5..0000000000
--- a/src/containers/Node/i18n/ru.json
+++ /dev/null
@@ -1,4 +0,0 @@
-{
- "pdisk.developer-ui-button-title": "Страница PDisk в Developer UI",
- "vdisk.developer-ui-button-title": "Страница VDisk в Developer UI"
-}
diff --git a/src/routes.ts b/src/routes.ts
index 06e211d3e3..056043b7b6 100644
--- a/src/routes.ts
+++ b/src/routes.ts
@@ -25,7 +25,6 @@ const routes = {
vDisk: `/${VDISK}`,
storageGroup: `/${STORAGE_GROUP}`,
tablet: `/${TABLET}/:id`,
- tabletsFilters: `/tabletsFilters`,
auth: `/auth`,
} as const;
@@ -58,7 +57,7 @@ type Query = AnyRecord;
export function createHref(
route: string,
- params?: Record,
+ params?: Record,
query: Query = {},
) {
let extendedQuery = query;
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
index 462b063085..4772624cf0 100644
--- a/src/utils/constants.ts
+++ b/src/utils/constants.ts
@@ -48,8 +48,6 @@ export const getTabletLabel = (type?: string) => {
return isTabletType(type) ? TABLET_SYMBOLS[type] : defaultValue;
};
-export const LOAD_AVERAGE_TIME_INTERVALS = ['1 min', '5 min', '15 min'];
-
export const TENANT_OVERVIEW_TABLES_LIMIT = 5;
export const EMPTY_DATA_PLACEHOLDER = '—';
diff --git a/src/utils/hooks/useNodeDeveloperUIHref.ts b/src/utils/hooks/useNodeDeveloperUIHref.ts
new file mode 100644
index 0000000000..4112703274
--- /dev/null
+++ b/src/utils/hooks/useNodeDeveloperUIHref.ts
@@ -0,0 +1,32 @@
+import {useAdditionalNodeProps} from '../../containers/AppWithClusters/useClusterData';
+import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication';
+import {useClusterBaseInfo} from '../../store/reducers/cluster/cluster';
+import type {PreparedNode} from '../../store/reducers/node/types';
+import {
+ createDeveloperUIInternalPageHref,
+ createDeveloperUILinkWithNodeId,
+} from '../developerUI/developerUI';
+
+import {useTypedSelector} from './useTypedSelector';
+
+export function useNodeDeveloperUIHref(node?: PreparedNode) {
+ const {balancer} = useClusterBaseInfo();
+ const {additionalNodesProps} = useAdditionalNodeProps({balancer});
+ const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges);
+
+ if (!isUserAllowedToMakeChanges) {
+ return undefined;
+ }
+
+ if (additionalNodesProps?.getNodeRef) {
+ const developerUIHref = additionalNodesProps.getNodeRef(node);
+ return developerUIHref ? createDeveloperUIInternalPageHref(developerUIHref) : undefined;
+ }
+
+ if (node?.NodeId) {
+ const developerUIHref = createDeveloperUILinkWithNodeId(node.NodeId);
+ return createDeveloperUIInternalPageHref(developerUIHref);
+ }
+
+ return undefined;
+}