From 861afab08d02e8ac481ec5d1f36035bd5eed92f3 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 7 Feb 2025 11:55:25 +0300 Subject: [PATCH 1/5] feat(Node): rework page --- .../BasicNodeViewer/BasicNodeViewer.scss | 35 --- .../BasicNodeViewer/BasicNodeViewer.tsx | 73 ----- src/components/BasicNodeViewer/index.ts | 1 - .../EntityPageTitle/EntityPageTitle.tsx | 11 +- .../FullNodeViewer/FullNodeViewer.scss | 12 +- .../FullNodeViewer/FullNodeViewer.tsx | 91 +++--- src/components/FullNodeViewer/i18n/en.json | 20 ++ src/components/FullNodeViewer/i18n/index.ts | 7 + src/components/PageMeta/PageMeta.tsx | 4 +- .../TabletsStatistic/TabletsStatistic.tsx | 4 +- src/components/VDisk/utils.ts | 12 +- .../AppWithClusters/AppWithClusters.tsx | 6 - .../ExtendedNode/ExtendedNode.tsx | 10 - src/containers/Header/breadcrumbs.tsx | 4 +- src/containers/Node/Node.scss | 62 ++--- src/containers/Node/Node.tsx | 258 +++++++++--------- src/containers/Node/NodePages.ts | 45 +-- src/containers/Node/i18n/en.json | 10 +- src/containers/Node/i18n/index.ts | 3 +- src/containers/Node/i18n/ru.json | 4 - src/routes.ts | 3 +- src/utils/constants.ts | 2 - src/utils/hooks/useNodeDeveloperUIHref.ts | 32 +++ 23 files changed, 328 insertions(+), 381 deletions(-) delete mode 100644 src/components/BasicNodeViewer/BasicNodeViewer.scss delete mode 100644 src/components/BasicNodeViewer/BasicNodeViewer.tsx delete mode 100644 src/components/BasicNodeViewer/index.ts create mode 100644 src/components/FullNodeViewer/i18n/en.json create mode 100644 src/components/FullNodeViewer/i18n/index.ts delete mode 100644 src/containers/AppWithClusters/ExtendedNode/ExtendedNode.tsx delete mode 100644 src/containers/Node/i18n/ru.json create mode 100644 src/utils/hooks/useNodeDeveloperUIHref.ts 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 ( -
- {node ? ( - -
Node
- - {developerUIInternalHref && isUserAllowedToMakeChanges ? ( - - - - ) : null} - -
- - -
- - {node.DC && } - {node.Roles && } -
- ) : ( -
no data
- )} -
- ); -}; diff --git a/src/components/BasicNodeViewer/index.ts b/src/components/BasicNodeViewer/index.ts deleted file mode 100644 index dcfd2526f8..0000000000 --- a/src/components/BasicNodeViewer/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './BasicNodeViewer'; diff --git a/src/components/EntityPageTitle/EntityPageTitle.tsx b/src/components/EntityPageTitle/EntityPageTitle.tsx index 29535e8dcf..92d52544f8 100644 --- a/src/components/EntityPageTitle/EntityPageTitle.tsx +++ b/src/components/EntityPageTitle/EntityPageTitle.tsx @@ -1,4 +1,4 @@ -import type {EFlag} from '../../types/api/enums'; +import {EFlag} from '../../types/api/enums'; import {cn} from '../../utils/cn'; import {StatusIcon} from '../StatusIcon/StatusIcon'; @@ -8,12 +8,17 @@ const b = cn('ydb-entity-page-title'); interface EntityPageTitleProps { entityName: React.ReactNode; - status: EFlag; + status?: EFlag; id: React.ReactNode; className?: string; } -export function EntityPageTitle({entityName, status, id, className}: EntityPageTitleProps) { +export function EntityPageTitle({ + entityName, + status = EFlag.Grey, + id, + className, +}: EntityPageTitleProps) { return (
{entityName} diff --git a/src/components/FullNodeViewer/FullNodeViewer.scss b/src/components/FullNodeViewer/FullNodeViewer.scss index 4a6f449147..b6ee9f944d 100644 --- a/src/components/FullNodeViewer/FullNodeViewer.scss +++ b/src/components/FullNodeViewer/FullNodeViewer.scss @@ -3,15 +3,11 @@ .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: 500px; &_pools { display: grid; @@ -26,8 +22,6 @@ } &__section-title { - margin: 15px 0 10px; - - font-weight: 600; + @include mixins.info-viewer-title(); } } diff --git a/src/components/FullNodeViewer/FullNodeViewer.tsx b/src/components/FullNodeViewer/FullNodeViewer.tsx index ae8d3a07f9..5b060d82c8 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,13 @@ export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => {
- {endpointsInfo && endpointsInfo.length && ( - - )} - - - -
- ) : ( -
no data
- )} + +
); }; diff --git a/src/components/FullNodeViewer/i18n/en.json b/src/components/FullNodeViewer/i18n/en.json new file mode 100644 index 0000000000..f52c016d87 --- /dev/null +++ b/src/components/FullNodeViewer/i18n/en.json @@ -0,0 +1,20 @@ +{ + "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.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/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..74ca975a53 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -2,14 +2,15 @@ import React from 'react'; import {Tabs} from '@gravity-ui/uikit'; 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 +18,15 @@ 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 {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,73 +34,107 @@ 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); + + // NodeId is always defined here because the page is wrapped with specific route Router + const nodeId = match?.params.id as string; + 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 { + currentData: node, + isLoading, + error, + } = nodeApi.useGetNodeInfoQuery({nodeId}, {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; + + const isStorageNode = node?.Roles?.find((el) => el === STORAGE_ROLE); - let nodePages = hasStorage ? NODE_PAGES : NODE_PAGES.filter((el) => el.id !== STORAGE); + 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 {activeTabVerified: actualActiveTab, nodeTabs: actualNodeTabs}; - }, [activeTab, node, isDiskPagesAvailable]); + const renderHelmet = () => { + const host = node?.Host ? node.Host : i18n('node'); + return ( + + {activeTab.title} + + ); + }; - const tenantName = node?.Tenants?.[0] || tenantNameFromQuery?.toString(); + const renderPageMeta = () => { + const hostItem = node?.Host ? `${i18n('fqdn')}: ${node.Host}` : undefined; + const dcItem = node?.DC ? `${i18n('dc')}: ${node.DC}` : undefined; - let nodeRole: 'Storage' | 'Compute' | undefined; - if (node) { - // Compute nodes have tenantName, storage nodes doesn't - const isStorage = !node?.Tenants?.[0]; - nodeRole = isStorage ? 'Storage' : 'Compute'; - } + return ( + + ); + }; - React.useEffect(() => { - dispatch( - setHeaderBreadcrumbs('node', { - tenantName, - nodeRole, - nodeId, - }), + const renderPageTitle = () => { + return ( + ); - }, [dispatch, tenantName, nodeId, nodeRole]); + }; + + const renderInfo = () => { + if (pageLoading) { + return ; + } + + return ; + }; const renderTabs = () => { return ( @@ -115,58 +142,43 @@ export function Node(props: NodeProps) { ( - - {tabNode} - - )} - allowNotSelected={true} + activeTab={activeTab.id} + 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 (activeTab.id) { + case 'storage': { return ( - ); } - - case STRUCTURE: { - return ; + case 'tablets': { + return ; } - case OVERVIEW: { - return ; + + case 'structure': { + return ; } default: @@ -174,35 +186,23 @@ export function Node(props: NodeProps) { } }; - if (loading || !capabilitiesLoaded) { - return ; - } - - if (node) { - return ( -
- - {activeTabVerified.title} - - - - {error ? : null} - {renderTabs()} -
{renderTabContent()}
-
- ); - } + const renderError = () => { + if (!error) { + return null; + } - if (error) { - return ; - } + return ; + }; - return
no node data
; + return ( +
+ {renderHelmet()} + {renderPageMeta()} + {renderPageTitle()} + {renderError()} + {renderInfo()} + {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; +} From 40005d30c46755700fa0dd279ff633d8f289af77 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Mon, 10 Feb 2025 12:39:22 +0300 Subject: [PATCH 2/5] fix: move node roles from meta to info --- src/components/FullNodeViewer/FullNodeViewer.tsx | 12 ++++++++++++ src/components/FullNodeViewer/i18n/en.json | 1 + src/containers/Node/Node.tsx | 2 +- 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/FullNodeViewer/FullNodeViewer.tsx b/src/components/FullNodeViewer/FullNodeViewer.tsx index 5b060d82c8..d797dbd7f0 100644 --- a/src/components/FullNodeViewer/FullNodeViewer.tsx +++ b/src/components/FullNodeViewer/FullNodeViewer.tsx @@ -45,6 +45,18 @@ export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => { if (node?.Rack) { commonInfo.push({label: i18n('rack'), value: node?.Rack}); } + if (node?.Roles && node?.Roles.length) { + commonInfo.push({ + label: i18n('roles'), + value: ( + + {node.Roles.map((role) => { + return
{role}
; + })} +
+ ), + }); + } if (developerUIHref) { commonInfo.push({ diff --git a/src/components/FullNodeViewer/i18n/en.json b/src/components/FullNodeViewer/i18n/en.json index f52c016d87..ecfadb83f0 100644 --- a/src/components/FullNodeViewer/i18n/en.json +++ b/src/components/FullNodeViewer/i18n/en.json @@ -4,6 +4,7 @@ "version": "Version", "dc": "DC", "rack": "Rack", + "roles": "Roles", "links": "Links", "la-interval-1m": "1 min", diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 74ca975a53..efa0f7bc8b 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -111,7 +111,7 @@ export function Node() { return ( ); From 61da0b06c46c4528ac2865e90d6b10f0ed1d131f Mon Sep 17 00:00:00 2001 From: mufazalov Date: Mon, 10 Feb 2025 13:04:06 +0300 Subject: [PATCH 3/5] fix: move node roles to a separate column --- .../FullNodeViewer/FullNodeViewer.scss | 8 +++++- .../FullNodeViewer/FullNodeViewer.tsx | 27 ++++++++++--------- src/components/FullNodeViewer/i18n/en.json | 2 +- 3 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/components/FullNodeViewer/FullNodeViewer.scss b/src/components/FullNodeViewer/FullNodeViewer.scss index b6ee9f944d..58f3a6b813 100644 --- a/src/components/FullNodeViewer/FullNodeViewer.scss +++ b/src/components/FullNodeViewer/FullNodeViewer.scss @@ -7,7 +7,9 @@ display: flex; flex-direction: column; - width: 500px; + width: max-content; + min-width: 300px; + max-width: 500px; &_pools { display: grid; @@ -24,4 +26,8 @@ &__section-title { @include mixins.info-viewer-title(); } + + &__role { + color: var(--g-color-text-secondary); + } } diff --git a/src/components/FullNodeViewer/FullNodeViewer.tsx b/src/components/FullNodeViewer/FullNodeViewer.tsx index d797dbd7f0..2251740f42 100644 --- a/src/components/FullNodeViewer/FullNodeViewer.tsx +++ b/src/components/FullNodeViewer/FullNodeViewer.tsx @@ -45,18 +45,6 @@ export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => { if (node?.Rack) { commonInfo.push({label: i18n('rack'), value: node?.Rack}); } - if (node?.Roles && node?.Roles.length) { - commonInfo.push({ - label: i18n('roles'), - value: ( - - {node.Roles.map((role) => { - return
{role}
; - })} -
- ), - }); - } if (developerUIHref) { commonInfo.push({ @@ -83,7 +71,7 @@ export const FullNodeViewer = ({node, className}: FullNodeViewerProps) => { return (
- + { info={averageInfo} /> + + {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 index ecfadb83f0..d20f622645 100644 --- a/src/components/FullNodeViewer/i18n/en.json +++ b/src/components/FullNodeViewer/i18n/en.json @@ -4,7 +4,6 @@ "version": "Version", "dc": "DC", "rack": "Rack", - "roles": "Roles", "links": "Links", "la-interval-1m": "1 min", @@ -16,6 +15,7 @@ "title.common-info": "Common info", "title.endpoints": "Endpoints", + "title.roles": "Roles", "title.pools": "Pools", "title.load-average": "Load average" } From e7eec6bb6fd6019ce3e89808ecf269be78e0b8b5 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Mon, 10 Feb 2025 13:42:40 +0300 Subject: [PATCH 4/5] fix: navigate to Node page storage if storage node --- src/components/NodeHostWrapper/NodeHostWrapper.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) 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 ( From f6417635d2dce3fe6790eef70d23855571a4a029 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Mon, 10 Feb 2025 13:43:00 +0300 Subject: [PATCH 5/5] fix: review --- src/containers/Node/Node.tsx | 169 ++++++++++++++++++++++------------- 1 file changed, 108 insertions(+), 61 deletions(-) diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index efa0f7bc8b..778e19c301 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -1,6 +1,8 @@ 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 {useRouteMatch} from 'react-router-dom'; import {useQueryParams} from 'use-query-params'; @@ -18,6 +20,7 @@ import { } from '../../store/reducers/capabilities/hooks'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {nodeApi} from '../../store/reducers/node/node'; +import type {PreparedNode} from '../../store/reducers/node/types'; import {cn} from '../../utils/cn'; import {useAutoRefreshInterval, useTypedDispatch} from '../../utils/hooks'; import {PaginatedStorage} from '../Storage/PaginatedStorage'; @@ -41,8 +44,7 @@ export function Node() { const match = useRouteMatch<{id: string; activeTab: string}>(routes.node); - // NodeId is always defined here because the page is wrapped with specific route Router - const nodeId = match?.params.id as string; + const nodeId = match?.params.id; const activeTabIdFromQuery = match?.params.activeTab; const [{database: tenantNameFromQuery}] = useQueryParams(nodePageQueryParams); @@ -50,11 +52,13 @@ export function Node() { const activeTabId = nodePageTabSchema.parse(activeTabIdFromQuery); const [autoRefreshInterval] = useAutoRefreshInterval(); + + const params = nodeId ? {nodeId} : skipToken; const { currentData: node, isLoading, error, - } = nodeApi.useGetNodeInfoQuery({nodeId}, {pollingInterval: autoRefreshInterval}); + } = nodeApi.useGetNodeInfoQuery(params, {pollingInterval: autoRefreshInterval}); const capabilitiesLoaded = useCapabilitiesLoaded(); const isDiskPagesAvailable = useDiskPagesAvailable(); @@ -92,57 +96,113 @@ export function Node() { } }, [dispatch, tenantName, nodeId, isLoading, isStorageNode]); - const renderHelmet = () => { - const host = node?.Host ? node.Host : i18n('node'); - return ( - - {activeTab.title} - - ); - }; + return ( +
+ {} + {} + {} + {error ? : null} + {} + {nodeId ? ( + + ) : null} +
+ ); +} - const renderPageMeta = () => { - const hostItem = node?.Host ? `${i18n('fqdn')}: ${node.Host}` : undefined; - const dcItem = node?.DC ? `${i18n('dc')}: ${node.DC}` : undefined; +interface NodePageHelmetProps { + node?: PreparedNode; + activeTabTitle?: string; +} - return ( - - ); - }; +function NodePageHelmet({node, activeTabTitle}: NodePageHelmetProps) { + const host = node?.Host ? node.Host : i18n('node'); + return ( + + {activeTabTitle} + + ); +} - const renderPageTitle = () => { - return ( - - ); - }; +interface NodePageMetaProps { + node?: PreparedNode; + loading?: boolean; +} - const renderInfo = () => { - if (pageLoading) { - return ; - } +function NodePageMeta({node, loading}: NodePageMetaProps) { + const hostItem = node?.Host ? `${i18n('fqdn')}: ${node.Host}` : undefined; + const dcItem = node?.DC ? `${i18n('dc')}: ${node.DC}` : undefined; - return ; - }; + return ( + + ); +} + +interface NodePageTitleProps { + node?: PreparedNode; +} + +function NodePageTitle({node}: NodePageTitleProps) { + return ( + + ); +} + +interface NodePageInfoProps { + node?: PreparedNode; + loading?: boolean; +} + +function NodePageInfo({node, loading}: NodePageInfoProps) { + if (loading) { + return ; + } + 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 (
{ const path = getDefaultNodePath( nodeId, @@ -161,14 +221,14 @@ export function Node() { }; const renderTabContent = () => { - switch (activeTab.id) { + switch (activeTabId) { case 'storage': { return ( ); @@ -186,23 +246,10 @@ export function Node() { } }; - const renderError = () => { - if (!error) { - return null; - } - - return ; - }; - return ( -
- {renderHelmet()} - {renderPageMeta()} - {renderPageTitle()} - {renderError()} - {renderInfo()} + {renderTabs()} {renderTabContent()} -
+ ); }