import React from 'react'; import {Tab, TabList, TabProvider} from '@gravity-ui/uikit'; import {skipToken} from '@reduxjs/toolkit/query'; import {Helmet} from 'react-helmet-async'; import {useHistory, useRouteMatch} from 'react-router-dom'; import {useQueryParams} from 'use-query-params'; import {StickyAutoRefreshControl} from '../../components/AutoRefreshControl/StickyAutoRefreshControl'; import {EntityPageTitle} from '../../components/EntityPageTitle/EntityPageTitle'; import {ResponseError} from '../../components/Errors/ResponseError'; import {FullNodeViewer} from '../../components/FullNodeViewer/FullNodeViewer'; import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewerSkeleton'; import {InternalLink} from '../../components/InternalLink'; import {PageMeta} from '../../components/PageMeta/PageMeta'; import routes, {getDefaultNodePath} from '../../routes'; import { useCapabilitiesLoaded, useConfigAvailable, useViewerPeersHandlerAvailable, } 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 {useIsViewerUser} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {checkIsStorageNode} from '../../utils/nodes'; import {useAppTitle} from '../App/AppTitleContext'; import {Configs} from '../Configs/Configs'; import {NodeNetwork} from '../Node/NodeNetwork/NodeNetwork'; import {PaginatedStorage} from '../Storage/PaginatedStorage'; import {Tablets} from '../Tablets/Tablets'; import type {NodeTab} from './NodePages'; import {NODE_TABS, nodePageQueryParams, nodePageTabSchema} from './NodePages'; import {Threads} from './Threads/Threads'; import i18n from './i18n'; import './Node.scss'; const b = cn('node'); export function Node() { const container = React.useRef(null); const isViewerUser = useIsViewerUser(); const hasConfigs = useConfigAvailable(); const configsAvailable = isViewerUser && hasConfigs; const dispatch = useTypedDispatch(); const history = useHistory(); const match = useRouteMatch<{id: string; activeTab: string; environment?: string}>(routes.node); const nodeId = match?.params.id; const activeTabIdFromQuery = match?.params.activeTab; const environment = match?.params.environment; const [{database: tenantNameFromQuery, clusterName, backend}] = useQueryParams(nodePageQueryParams); const database = tenantNameFromQuery?.toString(); const activeTabId = nodePageTabSchema.parse(activeTabIdFromQuery); const [autoRefreshInterval] = useAutoRefreshInterval(); const params = nodeId ? {nodeId, database} : skipToken; const { currentData: node, isLoading, error, } = nodeApi.useGetNodeInfoQuery(params, {pollingInterval: autoRefreshInterval}); const capabilitiesLoaded = useCapabilitiesLoaded(); const isPeersHandlerAvailable = useViewerPeersHandlerAvailable(); const pageLoading = isLoading || !capabilitiesLoaded; const isStorageNode = checkIsStorageNode(node); const hasThreads = Boolean(node?.Threads?.length); const {activeTab, nodeTabs} = React.useMemo(() => { const skippedTabs: NodeTab[] = []; if (!isStorageNode) { skippedTabs.push('storage'); } if (!configsAvailable) { skippedTabs.push('configs'); } if ((!database && !isViewerUser) || !hasThreads) { skippedTabs.push('threads'); } if (!isViewerUser || !isPeersHandlerAvailable) { skippedTabs.push('network'); } let actualNodeTabs = NODE_TABS; // Return all tabs during loading, filter based on capabilities after loading completes if (!pageLoading) { actualNodeTabs = NODE_TABS.filter((el) => !skippedTabs.includes(el.id)); } const actualActiveTab = actualNodeTabs.find(({id}) => id === activeTabId) ?? actualNodeTabs[0]; return {activeTab: actualActiveTab, nodeTabs: actualNodeTabs}; }, [ isStorageNode, isPeersHandlerAvailable, activeTabId, hasThreads, configsAvailable, isViewerUser, database, pageLoading, ]); const databaseName = node?.Tenants?.[0]; React.useEffect(() => { // Dispatch only if loaded to get correct node role if (!isLoading) { dispatch( setHeaderBreadcrumbs('node', { database, databaseName, nodeRole: isStorageNode ? 'Storage' : 'Compute', nodeId, }), ); } }, [dispatch, database, nodeId, isLoading, isStorageNode, databaseName]); React.useEffect(() => { if (!nodeId || !activeTab) { return; } if (activeTab.id !== activeTabId || activeTabIdFromQuery === 'structure') { const path = getDefaultNodePath( {id: nodeId, activeTab: activeTab.id, environment}, {database, clusterName: clusterName ?? undefined, backend: backend ?? undefined}, ); history.replace(path); } }, [ nodeId, database, clusterName, backend, environment, activeTab.id, activeTabId, activeTabIdFromQuery, history, activeTab, ]); return (
{} {} {} {error ? : null} {} {!pageLoading && nodeId ? ( ) : null}
); } interface NodePageHelmetProps { node?: PreparedNode; activeTabTitle?: string; } function NodePageHelmet({node, activeTabTitle}: NodePageHelmetProps) { const {appTitle} = useAppTitle(); const host = node?.Host ? node.Host : i18n('node'); return ( {activeTabTitle} ); } interface NodePageMetaProps { node?: PreparedNode; loading?: boolean; } 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 ; } return ; } interface NodePageContentProps { nodeId: string; database?: string; clusterName?: string; backend?: string; environment?: string; activeTabId: NodeTab; tabs: {id: string; title: string}[]; parentContainer: React.RefObject; } function NodePageContent({ nodeId, database, clusterName, backend, environment, activeTabId, tabs, parentContainer, }: NodePageContentProps) { const renderTabs = () => { return (
{tabs.map(({id, title}) => { const path = getDefaultNodePath( {id: nodeId, activeTab: id as NodeTab, environment}, {database, clusterName, backend}, ); return ( {title} ); })}
); }; const renderTabContent = () => { switch (activeTabId) { case 'storage': { return ( ); } case 'tablets': { return ( ); } case 'threads': { return ( ); } case 'configs': { return ; } case 'network': { return ; } default: return false; } }; return ( {renderTabs()} {renderTabContent()} ); }