diff --git a/src/components/NodeHostWrapper/NodeHostWrapper.tsx b/src/components/NodeHostWrapper/NodeHostWrapper.tsx index fab952f3de..f3dfb8f075 100644 --- a/src/components/NodeHostWrapper/NodeHostWrapper.tsx +++ b/src/components/NodeHostWrapper/NodeHostWrapper.tsx @@ -1,4 +1,4 @@ -import {getDefaultNodePath} from '../../containers/Node/NodePages'; +import {getDefaultNodePath} from '../../routes'; import type {PreparedStorageNode} from '../../store/reducers/storage/types'; import type {NodeAddress} from '../../types/additionalProps'; import type {TNodeInfo, TSystemStateInfo} from '../../types/api/nodes'; @@ -35,11 +35,10 @@ export const NodeHostWrapper = ({ const nodePath = isNodeAvailable ? getDefaultNodePath( - node.NodeId, + {id: node.NodeId, activeTab: node.TenantName ? 'tablets' : 'storage'}, { database: database ?? node.TenantName, }, - node.TenantName ? 'tablets' : 'storage', ) : undefined; diff --git a/src/components/NodeId/NodeId.tsx b/src/components/NodeId/NodeId.tsx index d5028478a9..ebea7acb3d 100644 --- a/src/components/NodeId/NodeId.tsx +++ b/src/components/NodeId/NodeId.tsx @@ -1,4 +1,4 @@ -import {getDefaultNodePath} from '../../containers/Node/NodePages'; +import {getDefaultNodePath} from '../../routes'; import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery'; import {InternalLink} from '../InternalLink'; @@ -8,5 +8,5 @@ interface NodeIdProps { export function NodeId({id}: NodeIdProps) { const database = useDatabaseFromQuery(); - return {id}; + return {id}; } diff --git a/src/components/PDiskInfo/PDiskInfo.tsx b/src/components/PDiskInfo/PDiskInfo.tsx index a8bf93082f..58085cc655 100644 --- a/src/components/PDiskInfo/PDiskInfo.tsx +++ b/src/components/PDiskInfo/PDiskInfo.tsx @@ -1,6 +1,5 @@ import {Flex} from '@gravity-ui/uikit'; -import {getPDiskPagePath} from '../../routes'; import {valueIsDefined} from '../../utils'; import {formatBytes} from '../../utils/bytesParsers'; import {formatStorageValuesToGb} from '../../utils/dataFormatters/dataFormatters'; @@ -10,6 +9,7 @@ import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedT import type {InfoViewerItem} from '../InfoViewer'; import {InfoViewer} from '../InfoViewer/InfoViewer'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; +import {PDiskPageLink} from '../PDiskPageLink/PDiskPageLink'; import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; import {StatusIcon} from '../StatusIcon/StatusIcon'; @@ -149,7 +149,6 @@ function getPDiskInfo({ valueIsDefined(nodeId); if (shouldDisplayLinks) { - const pDiskPagePath = getPDiskPagePath(PDiskId, nodeId); const pDiskInternalViewerPath = createPDiskDeveloperUILink({ nodeId, pDiskId: PDiskId, @@ -159,13 +158,7 @@ function getPDiskInfo({ label: pDiskInfoKeyset('links'), value: ( - {withPDiskPageLink && ( - - )} + {withPDiskPageLink && } {isUserAllowedToMakeChanges && ( + ); +} diff --git a/src/components/PDiskPageLink/i18n/en.json b/src/components/PDiskPageLink/i18n/en.json new file mode 100644 index 0000000000..d8783b862c --- /dev/null +++ b/src/components/PDiskPageLink/i18n/en.json @@ -0,0 +1,3 @@ +{ + "pdisk-page": "PDisk page" +} diff --git a/src/components/PDiskPageLink/i18n/index.ts b/src/components/PDiskPageLink/i18n/index.ts new file mode 100644 index 0000000000..97f1d50a59 --- /dev/null +++ b/src/components/PDiskPageLink/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-pDisk-link'; + +export const i18n = registerKeysets(COMPONENT, {en}); diff --git a/src/components/PDiskPopup/PDiskPopup.tsx b/src/components/PDiskPopup/PDiskPopup.tsx index aff71675df..9bf2493369 100644 --- a/src/components/PDiskPopup/PDiskPopup.tsx +++ b/src/components/PDiskPopup/PDiskPopup.tsx @@ -2,7 +2,6 @@ import React from 'react'; import {Flex} from '@gravity-ui/uikit'; -import {getPDiskPagePath} from '../../routes'; import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; import {valueIsDefined} from '../../utils'; @@ -17,6 +16,7 @@ import {InfoViewer} from '../InfoViewer'; import type {InfoViewerItem} from '../InfoViewer'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; import {pDiskInfoKeyset} from '../PDiskInfo/i18n'; +import {PDiskPageLink} from '../PDiskPageLink/PDiskPageLink'; const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow]; @@ -83,16 +83,11 @@ export const preparePDiskData = ( pDiskId: PDiskId, }); - const pDiskPagePath = getPDiskPagePath(PDiskId, NodeId); pdiskData.push({ label: 'Links', value: ( - + , 'columnsWidthLSKey' | 'columns'> { columnsIds: TopShardsColumnId[]; - database: string; databaseFullPath: string; overrideColumns?: ShardsColumn[]; } export function ShardsTable({ columnsIds, - database, databaseFullPath, overrideColumns = [], ...props @@ -33,14 +31,14 @@ export function ShardsTable({ return overridedColumn; } - const column = shardsColumnIdToGetColumn[id]({database, databaseFullPath}); + const column = shardsColumnIdToGetColumn[id]({databaseFullPath}); return { ...column, sortable: isSortableTopShardsColumn(column.name), }; }); - }, [columnsIds, database, overrideColumns, databaseFullPath]); + }, [columnsIds, overrideColumns, databaseFullPath]); return ( { align: DataTable.RIGHT, }; }; -export const getTabletIdColumn: GetShardsColumn = ({database}) => { +export const getTabletIdColumn: GetShardsColumn = () => { return { name: TOP_SHARDS_COLUMNS_IDS.TabletId, header: TOP_SHARDS_COLUMNS_TITLES.TabletId, @@ -50,7 +50,6 @@ export const getTabletIdColumn: GetShardsColumn = ({database}) => { ); }, diff --git a/src/components/ShardsTable/types.ts b/src/components/ShardsTable/types.ts index b4ca98afb3..245ca2f8a0 100644 --- a/src/components/ShardsTable/types.ts +++ b/src/components/ShardsTable/types.ts @@ -4,7 +4,4 @@ import type {KeyValueRow} from '../../types/api/query'; export type ShardsColumn = Column; -export type GetShardsColumn = (params: { - database: string; - databaseFullPath?: string; -}) => ShardsColumn; +export type GetShardsColumn = (params: {databaseFullPath?: string}) => ShardsColumn; diff --git a/src/components/TabletNameWrapper/TabletNameWrapper.tsx b/src/components/TabletNameWrapper/TabletNameWrapper.tsx index be7f204540..1478aa3817 100644 --- a/src/components/TabletNameWrapper/TabletNameWrapper.tsx +++ b/src/components/TabletNameWrapper/TabletNameWrapper.tsx @@ -1,14 +1,14 @@ -import {getTabletPagePath} from '../../routes'; +import {useTabletPagePath} from '../../routes'; import {EntityStatus} from '../EntityStatus/EntityStatus'; interface TabletNameWrapperProps { tabletId: string | number; followerId?: string | number; - database?: string; } -export function TabletNameWrapper({tabletId, followerId, database}: TabletNameWrapperProps) { - const tabletPath = getTabletPagePath(tabletId, {database, followerId: followerId?.toString()}); +export function TabletNameWrapper({tabletId, followerId}: TabletNameWrapperProps) { + const getTabletPagePath = useTabletPagePath(); + const tabletPath = getTabletPagePath(tabletId, {followerId: followerId?.toString()}); const tabletName = `${tabletId}${followerId ? `.${followerId}` : ''}`; return ( diff --git a/src/components/TabletsStatistic/TabletsStatistic.tsx b/src/components/TabletsStatistic/TabletsStatistic.tsx index 6515a91150..754ba6bd6b 100644 --- a/src/components/TabletsStatistic/TabletsStatistic.tsx +++ b/src/components/TabletsStatistic/TabletsStatistic.tsx @@ -1,6 +1,6 @@ import {Link} from 'react-router-dom'; -import {getDefaultNodePath} from '../../containers/Node/NodePages'; +import {getDefaultNodePath} from '../../routes'; 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({id: nodeId, activeTab: 'tablets'}, {database}); const label = `${item.label}: ${item.count}`; const className = b('tablet', {state: item.state?.toLowerCase()}); diff --git a/src/components/TenantNameWrapper/TenantNameWrapper.tsx b/src/components/TenantNameWrapper/TenantNameWrapper.tsx index c66b200d2f..f26bc07c34 100644 --- a/src/components/TenantNameWrapper/TenantNameWrapper.tsx +++ b/src/components/TenantNameWrapper/TenantNameWrapper.tsx @@ -1,6 +1,6 @@ import {DefinitionList, Flex} from '@gravity-ui/uikit'; -import {getTenantPath} from '../../containers/Tenant/TenantPages'; +import {getTenantPath} from '../../routes'; import type {PreparedTenant} from '../../store/reducers/tenants/types'; import type {AdditionalTenantsProps} from '../../types/additionalProps'; import {uiFactory} from '../../uiFactory/uiFactory'; diff --git a/src/components/VDisk/VDisk.tsx b/src/components/VDisk/VDisk.tsx index 2d6bbd3063..0459bf816b 100644 --- a/src/components/VDisk/VDisk.tsx +++ b/src/components/VDisk/VDisk.tsx @@ -1,13 +1,11 @@ +import {useVDiskPagePath} from '../../routes'; import {cn} from '../../utils/cn'; import type {PreparedVDisk} from '../../utils/disks/types'; -import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery'; import {DiskStateProgressBar} from '../DiskStateProgressBar/DiskStateProgressBar'; import {HoverPopup} from '../HoverPopup/HoverPopup'; import {InternalLink} from '../InternalLink'; import {VDiskPopup} from '../VDiskPopup/VDiskPopup'; -import {getVDiskLink} from './utils'; - import './VDisk.scss'; const b = cn('ydb-vdisk-component'); @@ -35,10 +33,8 @@ export const VDisk = ({ delayClose, delayOpen, }: VDiskProps) => { - const database = useDatabaseFromQuery(); - const vDiskPath = getVDiskLink(data, { - database, - }); + const getVDiskLink = useVDiskPagePath(); + const vDiskPath = getVDiskLink({nodeId: data.NodeId, vDiskId: data.StringifiedId}); return ( ({ wrap, }: VDiskInfoProps) { const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); - const database = useDatabaseFromQuery(); + const getVDiskPagePath = useVDiskPagePath(); const { AllocatedSize, @@ -179,7 +178,7 @@ export function VDiskInfo({ rightColumn.push({label: vDiskInfoKeyset('slot-id'), value: VDiskSlotId}); } if (!isNil(PDiskId)) { - const pDiskPath = !isNil(NodeId) ? getPDiskPagePath(PDiskId, NodeId) : undefined; + const pDiskPath = isNil(NodeId) ? undefined : getPDiskPagePath(PDiskId, NodeId); const value = pDiskPath ? {PDiskId} : PDiskId; @@ -217,13 +216,10 @@ export function VDiskInfo({ return null; } - const vDiskPath = getVDiskPagePath( - { - nodeId: dNodeId, - vDiskId: id, - }, - {database}, - ); + const vDiskPath = getVDiskPagePath({ + nodeId: dNodeId, + vDiskId: id, + }); return ( @@ -244,24 +240,19 @@ export function VDiskInfo({ } } const links: React.ReactNode[] = []; - if (!isNil(StringifiedId)) { - if (withVDiskPageLink) { - const vDiskPagePath = getVDiskPagePath( - { - nodeId: NodeId, - vDiskId: StringifiedId, - }, - {database}, - ); - links.push( - , - ); - } + const vDiskPagePath = getVDiskPagePath({ + nodeId: NodeId, + vDiskId: StringifiedId, + }); + if (withVDiskPageLink && vDiskPagePath) { + links.push( + , + ); } if (isUserAllowedToMakeChanges && !isNil(NodeId) && !isNil(VDiskSlotId) && !isNil(PDiskId)) { diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index dccb4040e5..7e61bb28a4 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {Flex, Label} from '@gravity-ui/uikit'; +import {useVDiskPagePath} from '../../routes'; import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; import {EVDiskState} from '../../types/api/vdisk'; @@ -24,7 +25,6 @@ import {InfoViewer} from '../InfoViewer'; import {InternalLink} from '../InternalLink'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; import {preparePDiskData} from '../PDiskPopup/PDiskPopup'; -import {getVDiskLink} from '../VDisk/utils'; import {vDiskInfoKeyset} from '../VDiskInfo/i18n'; import {vDiskPopupKeyset} from './i18n'; @@ -75,7 +75,10 @@ const prepareUnavailableVDiskData = (data: UnavailableDonor, withDeveloperUILink const prepareVDiskData = ( data: PreparedVDisk, withDeveloperUILink: boolean | undefined, - query: {database: string | undefined}, + getVDiskLinkFn?: (data: { + nodeId: string | number; + vDiskId: string | undefined; + }) => string | undefined, ) => { const { NodeId, @@ -94,7 +97,6 @@ const prepareVDiskData = ( ReadThroughput, WriteThroughput, StoragePoolName, - VDiskId, } = data; const vdiskData: InfoViewerItem[] = [ @@ -207,10 +209,7 @@ const prepareVDiskData = ( }) : undefined; - const vDiskPagePath = getVDiskLink( - {VDiskSlotId, PDiskId, NodeId, StringifiedId, VDiskId}, - query, - ); + const vDiskPagePath = getVDiskLinkFn?.({nodeId: NodeId, vDiskId: StringifiedId}); if (vDiskPagePath) { vdiskData.push({ label: vDiskPopupKeyset('label_links'), @@ -246,15 +245,16 @@ export const VDiskPopup = ({data}: VDiskPopupProps) => { const isViewerUser = useIsViewerUser(); const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); + const getVDiskLink = useVDiskPagePath(); const database = useDatabaseFromQuery(); const vdiskInfo = React.useMemo( () => isFullData - ? prepareVDiskData(data, isUserAllowedToMakeChanges, {database}) + ? prepareVDiskData(data, isUserAllowedToMakeChanges, getVDiskLink) : prepareUnavailableVDiskData(data, isUserAllowedToMakeChanges), - [data, isFullData, isUserAllowedToMakeChanges, database], + [data, isFullData, isUserAllowedToMakeChanges, getVDiskLink], ); const nodesMap = useTypedSelector((state) => selectNodesMap(state, database)); @@ -274,7 +274,9 @@ export const VDiskPopup = ({data}: VDiskPopupProps) => { donorsInfo.push({ label: vDiskPopupKeyset('label_vdisk'), value: ( - + {donor.StringifiedId} ), diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx index 315e8b59b6..f56c9dd047 100644 --- a/src/containers/App/Content.tsx +++ b/src/containers/App/Content.tsx @@ -10,7 +10,7 @@ import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; import {useSlots} from '../../components/slots'; import type {SlotMap} from '../../components/slots/SlotMap'; import type {SlotComponent} from '../../components/slots/types'; -import routes from '../../routes'; +import routes, {getClusterPath} from '../../routes'; import type {RootState} from '../../store'; import {authenticationApi} from '../../store/reducers/authentication/authentication'; import { @@ -27,7 +27,6 @@ import {useMetaAuth, useMetaAuthUnavailable} from '../../utils/hooks/useMetaAuth import {lazyComponent} from '../../utils/lazyComponent'; import {isAccessError, isRedirectToAuth} from '../../utils/response'; import Authentication from '../Authentication/Authentication'; -import {getClusterPath} from '../Cluster/utils'; import Header from '../Header/Header'; import {useAppTitle} from './AppTitleContext'; diff --git a/src/containers/Cluster/Cluster.tsx b/src/containers/Cluster/Cluster.tsx index 0fc43c2422..5b3aa40b21 100644 --- a/src/containers/Cluster/Cluster.tsx +++ b/src/containers/Cluster/Cluster.tsx @@ -12,7 +12,7 @@ import {EFlagToDescription} from '../../components/EntityStatusNew/utils'; import {InternalLink} from '../../components/InternalLink'; import {NetworkTable} from '../../components/NetworkTable/NetworkTable'; import {useShouldShowClusterNetworkTable} from '../../components/NetworkTable/hooks'; -import routes, {getLocationObjectFromHref} from '../../routes'; +import routes, {getClusterPath, getLocationObjectFromHref} from '../../routes'; import { useClusterDashboardAvailable, useConfigAvailable, @@ -42,13 +42,7 @@ import {VersionsContainer} from '../Versions/Versions'; import {ClusterOverview} from './ClusterOverview/ClusterOverview'; import type {ClusterTab} from './utils'; -import { - clusterTabs, - clusterTabsIds, - getClusterPath, - isClusterTab, - useShouldShowEventsTab, -} from './utils'; +import {clusterTabs, clusterTabsIds, isClusterTab, useShouldShowEventsTab} from './utils'; import './Cluster.scss'; @@ -175,10 +169,13 @@ export function Cluster({additionalClusterProps, additionalTenantsProps}: Cluste {actualClusterTabs.map(({id, title}) => { - const path = getClusterPath(id as ClusterTab, { - clusterName, - backend, - }); + const path = getClusterPath( + {activeTab: id as ClusterTab}, + { + clusterName, + backend, + }, + ); return ( @@ -242,7 +243,7 @@ export function Cluster({additionalClusterProps, additionalTenantsProps}: Cluste @@ -251,8 +252,9 @@ export function Cluster({additionalClusterProps, additionalTenantsProps}: Cluste )} @@ -260,8 +262,9 @@ export function Cluster({additionalClusterProps, additionalTenantsProps}: Cluste {shouldShowEventsTab && ( {uiFactory.renderEvents?.({scrollContainerRef: container})} @@ -271,7 +274,7 @@ export function Cluster({additionalClusterProps, additionalTenantsProps}: Cluste @@ -285,7 +288,9 @@ export function Cluster({additionalClusterProps, additionalTenantsProps}: Cluste ( )} /> diff --git a/src/containers/Cluster/utils.tsx b/src/containers/Cluster/utils.tsx index 4846786a61..8530250e61 100644 --- a/src/containers/Cluster/utils.tsx +++ b/src/containers/Cluster/utils.tsx @@ -1,5 +1,3 @@ -import type {CreateHrefOptions} from '../../routes'; -import routes, {createHref} from '../../routes'; import {useClusterEventsAvailable} from '../../store/reducers/capabilities/hooks'; import type {ClusterGroupsStats} from '../../store/reducers/cluster/types'; import type {ValueOf} from '../../types/common'; @@ -75,10 +73,6 @@ export function isClusterTab(tab: any): tab is ClusterTab { return Object.values(clusterTabsIds).includes(tab); } -export const getClusterPath = (activeTab?: ClusterTab, query = {}, options?: CreateHrefOptions) => { - return createHref(routes.cluster, activeTab ? {activeTab} : undefined, query, options); -}; - export const getTotalStorageGroupsUsed = (groupStats: ClusterGroupsStats) => { return Object.values(groupStats).reduce((acc, data) => { Object.values(data).forEach((erasureStats) => { diff --git a/src/containers/Clusters/columns.tsx b/src/containers/Clusters/columns.tsx index 82e0806bb4..301c2ad926 100644 --- a/src/containers/Clusters/columns.tsx +++ b/src/containers/Clusters/columns.tsx @@ -14,6 +14,7 @@ import { import {EntityStatus} from '../../components/EntityStatusNew/EntityStatus'; import {VersionsBar} from '../../components/VersionsBar/VersionsBar'; +import {getClusterPath} from '../../routes'; import type {PreparedCluster} from '../../store/reducers/clusters/types'; import {EFlag} from '../../types/api/enums'; import {uiFactory} from '../../uiFactory/uiFactory'; @@ -21,7 +22,7 @@ import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import {formatNumber, formatStorageValuesToTb} from '../../utils/dataFormatters/dataFormatters'; import {createDeveloperUIMonitoringPageHref} from '../../utils/developerUI/developerUI'; import {getCleanBalancerValue} from '../../utils/parseBalancer'; -import {clusterTabsIds, getClusterPath} from '../Cluster/utils'; +import {clusterTabsIds} from '../Cluster/utils'; import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants'; import i18n from './i18n'; @@ -43,17 +44,6 @@ function getTitleColumn({isEditClusterAvailable, isDeleteClusterAvailable}: Clus defaultOrder: DataTable.ASCENDING, sortAccessor: (row) => row.title || row.name, render: ({row}) => { - const { - name: clusterName, - use_embedded_ui: useEmbeddedUi, - preparedBackend: backend, - } = row; - - const clusterPath = - useEmbeddedUi && backend - ? createDeveloperUIMonitoringPageHref(backend) - : getClusterPath(undefined, {backend, clusterName}, {withBasename: true}); - const clusterStatus = row.cluster?.Overall; const cleanedBalancer = row.balancer ? getCleanBalancerValue(row.balancer) : null; @@ -98,11 +88,7 @@ function getTitleColumn({isEditClusterAvailable, isDeleteClusterAvailable}: Clus }; const renderName = () => { - return ( -
- {row.title || row.name} -
- ); + return ; }; const renderStatus = () => { @@ -153,6 +139,32 @@ function getTitleColumn({isEditClusterAvailable, isDeleteClusterAvailable}: Clus } satisfies Column; } +interface ClusterNameProps { + row: PreparedCluster; +} + +function ClusterName({row}: ClusterNameProps) { + const { + name: clusterName, + use_embedded_ui: useEmbeddedUi, + preparedBackend: backend, + settings, + } = row; + const clusterPath = + useEmbeddedUi && backend + ? createDeveloperUIMonitoringPageHref(backend) + : getClusterPath( + {environment: settings?.auth_service}, + {backend, clusterName}, + {withBasename: true}, + ); + return ( +
+ {row.title || row.name} +
+ ); +} + const CLUSTERS_COLUMNS: Column[] = [ { name: COLUMNS_NAMES.VERSIONS, @@ -167,12 +179,7 @@ const CLUSTERS_COLUMNS: Column[] = [ return versions[0] || undefined; }, render: ({row}) => { - const { - preparedVersions, - versions = [], - name: clusterName, - preparedBackend: backend, - } = row; + const {versions = []} = row; const hasErrors = !versions.length || versions.some((item) => !item.version); @@ -180,20 +187,7 @@ const CLUSTERS_COLUMNS: Column[] = [ return EMPTY_CELL; } - return ( - preparedVersions.length > 0 && ( - - - - ) - ); + return ; }, }, { @@ -359,6 +353,36 @@ const CLUSTERS_COLUMNS: Column[] = [ }, ]; +interface VersionsProps { + row: PreparedCluster; +} + +function Versions({row}: VersionsProps) { + const { + preparedVersions, + name: clusterName, + preparedBackend: backend, + settings, + use_embedded_ui: useEmbeddedUi, + } = row; + if (!preparedVersions.length) { + return null; + } + const clusterPath = + useEmbeddedUi && backend + ? createDeveloperUIMonitoringPageHref(backend) + : getClusterPath( + {activeTab: clusterTabsIds.versions, environment: settings?.auth_service}, + {backend, clusterName}, + {withBasename: true}, + ); + return ( + + + + ); +} + export function getClustersColumns(params: ClustersColumnsParams) { return [getTitleColumn(params), ...CLUSTERS_COLUMNS]; } diff --git a/src/containers/Header/Header.tsx b/src/containers/Header/Header.tsx index 0cb76136f8..172f94c7ef 100644 --- a/src/containers/Header/Header.tsx +++ b/src/containers/Header/Header.tsx @@ -15,7 +15,8 @@ import {useHistory, useLocation} from 'react-router-dom'; import {getConnectToDBDialog} from '../../components/ConnectToDB/ConnectToDBDialog'; import {InternalLink} from '../../components/InternalLink'; -import {checkIsClustersPage, checkIsTenantPage} from '../../routes'; +import {checkIsClustersPage, checkIsTenantPage, getClusterPath} from '../../routes'; +import {environment} from '../../store'; import { useAddClusterFeatureAvailable, useDatabasesAvailable, @@ -39,7 +40,6 @@ import { useIsViewerUser, } from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {isAccessError} from '../../utils/response'; -import {getClusterPath} from '../Cluster/utils'; import {getBreadcrumbs} from './breadcrumbs'; import {headerKeyset} from './i18n'; @@ -98,6 +98,7 @@ function Header() { ...pageBreadcrumbsOptions, singleClusterMode, isViewerUser, + environment, }; if (clusterTitle) { @@ -156,7 +157,7 @@ function Header() { action: () => { onDeleteDB({clusterName, databaseData}).then((isDeleted) => { if (isDeleted) { - const path = getClusterPath('tenants'); + const path = getClusterPath({activeTab: 'tenants'}); history.push(path); } }); diff --git a/src/containers/Header/breadcrumbs.tsx b/src/containers/Header/breadcrumbs.tsx index d347dbab04..8e3ef8bc0a 100644 --- a/src/containers/Header/breadcrumbs.tsx +++ b/src/containers/Header/breadcrumbs.tsx @@ -7,7 +7,13 @@ import { import {isNil} from 'lodash'; import {TabletIcon} from '../../components/TabletIcon/TabletIcon'; -import routes, {getPDiskPagePath, getStorageGroupPath} from '../../routes'; +import routes, { + getClusterPath, + getDefaultNodePath, + getPDiskPagePath, + getStorageGroupPath, + getTenantPath, +} from '../../routes'; import type { BreadcrumbsOptions, ClusterBreadcrumbsOptions, @@ -26,9 +32,7 @@ import { TENANT_PAGES_IDS, } from '../../store/reducers/tenant/constants'; import {CLUSTER_DEFAULT_TITLE, getTabletLabel} from '../../utils/constants'; -import {getClusterPath} from '../Cluster/utils'; -import {getDefaultNodePath} from '../Node/NodePages'; -import {TenantTabsGroups, getTenantPath} from '../Tenant/TenantPages'; +import {TenantTabsGroups} from '../Tenant/TenantPages'; import {headerKeyset} from './i18n'; @@ -63,7 +67,7 @@ const getClustersBreadcrumbs: GetBreadcrumbs = (opti }; const getClusterBreadcrumbs: GetBreadcrumbs = (options, query = {}) => { - const {clusterName, clusterTab, singleClusterMode, isViewerUser} = options; + const {clusterName, clusterTab, singleClusterMode, isViewerUser, environment} = options; if (!isViewerUser) { return []; @@ -77,7 +81,7 @@ const getClusterBreadcrumbs: GetBreadcrumbs = (option breadcrumbs.push({ text: clusterName || CLUSTER_DEFAULT_TITLE, - link: getClusterPath(clusterTab, query), + link: getClusterPath({activeTab: clusterTab, environment}, query), icon: , }); @@ -114,7 +118,9 @@ const getNodeBreadcrumbs: GetBreadcrumbs = (options, que const lastItem = { text, - link: nodeId ? getDefaultNodePath(nodeId, {database, ...query}, nodeActiveTab) : undefined, + link: nodeId + ? getDefaultNodePath({id: nodeId, activeTab: nodeActiveTab}, {database, ...query}) + : undefined, icon: getNodeIcon(nodeRole), }; diff --git a/src/containers/Heatmap/HeatmapCanvas/HeatmapCanvas.js b/src/containers/Heatmap/HeatmapCanvas/HeatmapCanvas.js index c955b97f49..1409e4878f 100644 --- a/src/containers/Heatmap/HeatmapCanvas/HeatmapCanvas.js +++ b/src/containers/Heatmap/HeatmapCanvas/HeatmapCanvas.js @@ -3,10 +3,9 @@ import React from 'react'; import throttle from 'lodash/throttle'; import PropTypes from 'prop-types'; -import {getTabletPagePath} from '../../../routes'; +import {useTabletPagePath} from '../../../routes'; import {basename as appBasename} from '../../../store/index'; import {cn} from '../../../utils/cn'; -import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery'; const b = cn('heatmap'); const defaultDimensions = {width: 0, height: 0}; @@ -19,7 +18,8 @@ export const HeatmapCanvas = (props) => { const {tablets} = props; const canvasRef = React.useRef(null); const containerRef = React.useRef(null); - const database = useDatabaseFromQuery(); + + const getTabletPagePath = useTabletPagePath(); function drawTablet(ctx) { return (tablet, index) => { @@ -92,7 +92,7 @@ export const HeatmapCanvas = (props) => { const generateTabletExternalLink = (tablet) => { const {TabletId: id} = tablet; const hostname = window.location.hostname; - const path = getTabletPagePath(id, {database}); + const path = getTabletPagePath(id); const protocol = 'https://'; const href = [hostname, appBasename, path] .map((item) => (item.startsWith('/') ? item.slice(1) : item)) diff --git a/src/containers/Node/Node.tsx b/src/containers/Node/Node.tsx index 09421b36fd..eec30bd049 100644 --- a/src/containers/Node/Node.tsx +++ b/src/containers/Node/Node.tsx @@ -12,7 +12,7 @@ import {FullNodeViewer} from '../../components/FullNodeViewer/FullNodeViewer'; import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewerSkeleton'; import {InternalLink} from '../../components/InternalLink'; import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta'; -import routes from '../../routes'; +import routes, {getDefaultNodePath} from '../../routes'; import { useCapabilitiesLoaded, useConfigAvailable, @@ -30,7 +30,7 @@ import {PaginatedStorage} from '../Storage/PaginatedStorage'; import {Tablets} from '../Tablets/Tablets'; import type {NodeTab} from './NodePages'; -import {NODE_TABS, getDefaultNodePath, nodePageQueryParams, nodePageTabSchema} from './NodePages'; +import {NODE_TABS, nodePageQueryParams, nodePageTabSchema} from './NodePages'; import NodeStructure from './NodeStructure/NodeStructure'; import {Threads} from './Threads/Threads'; import i18n from './i18n'; @@ -221,7 +221,10 @@ function NodePageContent({ {tabs.map(({id, title}) => { - const path = getDefaultNodePath(nodeId, {database}, id as NodeTab); + const path = getDefaultNodePath( + {id: nodeId, activeTab: id as NodeTab}, + {database}, + ); return ( diff --git a/src/containers/Node/NodePages.ts b/src/containers/Node/NodePages.ts index 2b8a5a5638..b3741a9f68 100644 --- a/src/containers/Node/NodePages.ts +++ b/src/containers/Node/NodePages.ts @@ -2,7 +2,6 @@ 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'; import i18n from './i18n'; @@ -58,19 +57,4 @@ export const nodePageQueryParams = { vdiskId: StringParam, }; -type NodePageQuery = QueryParamsTypeFromQueryObject; - -export function getDefaultNodePath( - nodeId: string | number, - query: NodePageQuery = {}, - activeTab?: NodeTab, -) { - return createHref( - routes.node, - { - id: nodeId, - activeTab, - }, - query, - ); -} +export type NodePageQuery = QueryParamsTypeFromQueryObject; diff --git a/src/containers/PDiskPage/PDiskSpaceDistribution/PDiskSpaceDistribution.tsx b/src/containers/PDiskPage/PDiskSpaceDistribution/PDiskSpaceDistribution.tsx index 5c39803d94..293595bbae 100644 --- a/src/containers/PDiskPage/PDiskSpaceDistribution/PDiskSpaceDistribution.tsx +++ b/src/containers/PDiskPage/PDiskSpaceDistribution/PDiskSpaceDistribution.tsx @@ -5,7 +5,7 @@ import {InfoViewer} from '../../../components/InfoViewer'; import {InternalLink} from '../../../components/InternalLink'; import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer'; import {VDiskInfo} from '../../../components/VDiskInfo/VDiskInfo'; -import {getVDiskPagePath} from '../../../routes'; +import {useVDiskPagePath} from '../../../routes'; import type { EmptySlotData, LogSlotData, @@ -32,6 +32,7 @@ interface PDiskSpaceDistributionProps { } export function PDiskSpaceDistribution({data}: PDiskSpaceDistributionProps) { + const getVDiskPagePath = useVDiskPagePath(); const {SlotItems} = data; const {PDiskId, NodeId} = data; @@ -40,7 +41,15 @@ export function PDiskSpaceDistribution({data}: PDiskSpaceDistributionProps) { const renderSlots = () => { return SlotItems?.map((item, index) => { - return ; + return ( + + ); }); }; @@ -72,17 +81,19 @@ interface SlotProps { pDiskId?: string | number; nodeId?: string | number; + getVDiskPagePath?: ( + params: {nodeId: string | number | undefined; vDiskId: string | undefined}, + query?: {activeTab?: string}, + ) => string | undefined; } -function Slot({item, nodeId}: SlotProps) { +function Slot({item, nodeId, getVDiskPagePath}: SlotProps) { const renderContent = () => { if (isVDiskSlot(item)) { - const vDiskPagePath = valueIsDefined(item.SlotData?.StringifiedId) - ? getVDiskPagePath({ - nodeId, - vDiskId: item.SlotData.StringifiedId, - }) - : undefined; + const vDiskPagePath = getVDiskPagePath?.({ + nodeId, + vDiskId: item.SlotData.StringifiedId, + }); return ( { }; function GroupId({id}: {id: string | number}) { - const database = useDatabaseFromQuery(); + const getStorageGroupPath = useStorageGroupPath(); return ( diff --git a/src/containers/Tablet/Tablet.tsx b/src/containers/Tablet/Tablet.tsx index da5964550c..3a140ec0bc 100644 --- a/src/containers/Tablet/Tablet.tsx +++ b/src/containers/Tablet/Tablet.tsx @@ -13,7 +13,7 @@ import {ResponseError} from '../../components/Errors/ResponseError'; import {InternalLink} from '../../components/InternalLink'; import {LoaderWrapper} from '../../components/LoaderWrapper/LoaderWrapper'; import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta'; -import {getTabletPagePath, tabletPageQueryParams} from '../../routes'; +import {tabletPageQueryParams, useTabletPagePath} from '../../routes'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; import {tabletApi} from '../../store/reducers/tablet'; import {EFlag} from '../../types/api/enums'; @@ -69,7 +69,7 @@ export function Tablet() { const database = queryDatabase?.toString(); const [autoRefreshInterval] = useAutoRefreshInterval(); const {currentData, isFetching, error} = tabletApi.useGetTabletQuery( - {id, database: queryDatabase ?? undefined, followerId: queryFollowerId ?? undefined}, + {id, database, followerId: queryFollowerId ?? undefined}, {pollingInterval: autoRefreshInterval}, ); @@ -114,9 +114,7 @@ export function Tablet() { {error ? : null} - {currentData ? ( - - ) : null} + {currentData ? : null}
); @@ -126,12 +124,10 @@ function TabletContent({ id, tablet, history, - database, }: { id: string; tablet: TTabletStateInfo; history: ITabletPreparedHistoryItem[]; - database?: string; }) { const isEmpty = !Object.keys(tablet).length; const {Overall, HiveId, FollowerId, Type} = tablet; @@ -153,7 +149,7 @@ function TabletContent({
- + ); } @@ -162,13 +158,12 @@ function TabletTabs({ id, hiveId, history, - database, }: { id: string; hiveId?: string; - database?: string; history: ITabletPreparedHistoryItem[]; }) { + const getTabletPagePath = useTabletPagePath(); const [{activeTab, ...restParams}, setParams] = useQueryParams(tabletPageQueryParams); const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); @@ -206,9 +201,7 @@ function TabletTabs({ - {tabletTab === 'history' ? ( - - ) : null} + {tabletTab === 'history' ? : null} {tabletTab === 'channels' && !noAdvancedInfo ? ( ) : null} diff --git a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx index 81f3836754..f48b6bd7cc 100644 --- a/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx +++ b/src/containers/Tablet/components/TabletInfo/TabletInfo.tsx @@ -6,14 +6,13 @@ import {InfoViewer} from '../../../../components/InfoViewer'; import {LinkWithIcon} from '../../../../components/LinkWithIcon/LinkWithIcon'; import {TabletState} from '../../../../components/TabletState/TabletState'; import {TabletUptime} from '../../../../components/UptimeViewer/UptimeViewer'; -import {getTabletPagePath} from '../../../../routes'; +import {getDefaultNodePath, useTabletPagePath} from '../../../../routes'; import {ETabletState} from '../../../../types/api/tablet'; import type {TTabletStateInfo} from '../../../../types/api/tablet'; import {cn} from '../../../../utils/cn'; import {createTabletDeveloperUIHref} from '../../../../utils/developerUI/developerUI'; import {useDatabaseFromQuery} from '../../../../utils/hooks/useDatabaseFromQuery'; import {useIsUserAllowedToMakeChanges} from '../../../../utils/hooks/useIsUserAllowedToMakeChanges'; -import {getDefaultNodePath} from '../../../Node/NodePages'; import {hasHive} from '../../utils'; import {tabletInfoKeyset} from './i18n'; @@ -27,6 +26,7 @@ interface TabletInfoProps { } export const TabletInfo = ({tablet}: TabletInfoProps) => { + const getTabletPagePath = useTabletPagePath(); const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); const database = useDatabaseFromQuery(); @@ -50,7 +50,7 @@ export const TabletInfo = ({tablet}: TabletInfoProps) => { tabletInfo.push({ label: tabletInfoKeyset('field_hive'), value: ( - + {HiveId} ), @@ -61,7 +61,7 @@ export const TabletInfo = ({tablet}: TabletInfoProps) => { tabletInfo.push({ label: tabletInfoKeyset('field_scheme-shard'), value: ( - + {SchemeShard} ), @@ -82,7 +82,10 @@ export const TabletInfo = ({tablet}: TabletInfoProps) => { { label: tabletInfoKeyset('field_node'), value: ( - + {NodeId} ), diff --git a/src/containers/Tablet/components/TabletTable/TabletTable.tsx b/src/containers/Tablet/components/TabletTable/TabletTable.tsx index 0f3101c38a..d7a67c37b3 100644 --- a/src/containers/Tablet/components/TabletTable/TabletTable.tsx +++ b/src/containers/Tablet/components/TabletTable/TabletTable.tsx @@ -10,15 +10,14 @@ import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/Re import {TabletState} from '../../../../components/TabletState/TabletState'; import {TabletUptime} from '../../../../components/UptimeViewer/UptimeViewer'; import {EMPTY_DATA_PLACEHOLDER} from '../../../../lib'; -import {getTabletPagePath} from '../../../../routes'; +import {useTabletPagePath} from '../../../../routes'; import type {ITabletPreparedHistoryItem} from '../../../../types/store/tablet'; const TABLET_COLUMNS_WIDTH_LS_KEY = 'tabletTableColumnsWidth'; -const getColumns: (props: { - database?: string; - tabletId: string; -}) => Column[] = ({database, tabletId}) => [ +const getColumns: (props: {tabletId: string}) => Column[] = ({ + tabletId, +}) => [ { name: 'Generation', align: DataTable.RIGHT, @@ -41,14 +40,7 @@ const getColumns: (props: { { name: 'Tablet', sortable: false, - render: ({row}) => { - const tabletPath = getTabletPagePath(tabletId, { - database, - followerId: row.leader ? undefined : row.followerId?.toString(), - }); - const tabletName = `${tabletId}${row.followerId ? `.${row.followerId}` : ''}`; - return {tabletName}; - }, + render: ({row}) => , }, { name: 'Node ID', @@ -71,6 +63,20 @@ const getColumns: (props: { }, ]; +interface TabletLinkProps { + row: ITabletPreparedHistoryItem; + tabletId: string; +} + +function TabletLink({tabletId, row}: TabletLinkProps) { + const getTabletPagePath = useTabletPagePath(); + const tabletPath = getTabletPagePath(tabletId, { + followerId: row.leader ? undefined : row.followerId?.toString(), + }); + const tabletName = `${tabletId}${row.followerId ? `.${row.followerId}` : ''}`; + return {tabletName}; +} + const TABLE_SETTINGS = { displayIndices: false, highlightRows: true, @@ -78,14 +84,13 @@ const TABLE_SETTINGS = { interface TabletTableProps { history: ITabletPreparedHistoryItem[]; - database?: string; tabletId: string; } -export const TabletTable = ({history, database, tabletId}: TabletTableProps) => { +export const TabletTable = ({history, tabletId}: TabletTableProps) => { const columns = React.useMemo(() => { - return getColumns({database, tabletId}); - }, [database, tabletId]); + return getColumns({tabletId}); + }, [tabletId]); return ( [] = [ { name: 'Type', @@ -56,7 +56,6 @@ function getColumns({database, nodeId}: {database?: string; nodeId?: string | nu return ( ); @@ -173,7 +172,6 @@ function TabletActions(tablet: TTabletStateInfo) { } interface TabletsTableProps { - database?: string; tablets: (TTabletStateInfo & { fqdn?: string; })[]; @@ -185,7 +183,6 @@ interface TabletsTableProps { } export function TabletsTable({ - database, tablets, loading, error, @@ -199,7 +196,7 @@ export function TabletsTable({ // Track sort state for scroll dependencies const [sortParams, setSortParams] = React.useState(); - const columns = React.useMemo(() => getColumns({database, nodeId}), [database, nodeId]); + const columns = React.useMemo(() => getColumns({nodeId}), [nodeId]); const filteredTablets = React.useMemo(() => { return tablets.filter((tablet) => { diff --git a/src/containers/Tenant/Diagnostics/Consumers/columns/columns.tsx b/src/containers/Tenant/Diagnostics/Consumers/columns/columns.tsx index 9052e80949..f9569d138e 100644 --- a/src/containers/Tenant/Diagnostics/Consumers/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/Consumers/columns/columns.tsx @@ -5,11 +5,12 @@ import qs from 'qs'; import {InternalLink} from '../../../../../components/InternalLink'; import {SpeedMultiMeter} from '../../../../../components/SpeedMultiMeter'; import {EMPTY_DATA_PLACEHOLDER} from '../../../../../lib'; +import {getTenantPath} from '../../../../../routes'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import type {IPreparedConsumerData} from '../../../../../types/store/topic'; import {cn} from '../../../../../utils/cn'; import {formatMsToUptime} from '../../../../../utils/dataFormatters/dataFormatters'; -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantTabsGroups} from '../../../TenantPages'; import {ReadLagsHeader} from '../Headers'; import { CONSUMERS_COLUMNS_IDS, @@ -34,21 +35,7 @@ export const columns: Column[] = [ return EMPTY_DATA_PLACEHOLDER; } - const queryParams = qs.parse(location.search, { - ignoreQueryPrefix: true, - }); - - return ( - - {row.name} - - ); + return ; }, }, { @@ -91,3 +78,24 @@ export const columns: Column[] = [ ], }, ]; + +interface ConsumerProps { + name: string; +} + +function Consumer({name}: ConsumerProps) { + const queryParams = qs.parse(location.search, { + ignoreQueryPrefix: true, + }); + return ( + + {name} + + ); +} diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts index c6c81b8d34..3e0b4ff8a5 100644 --- a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts +++ b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts @@ -2,12 +2,13 @@ import React from 'react'; import {StringParam, useQueryParams} from 'use-query-params'; +import {getTenantPath} from '../../../routes'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../store/reducers/tenant/constants'; import type {TenantDiagnosticsTab} from '../../../store/reducers/tenant/types'; import {EPathSubType, EPathType} from '../../../types/api/schema'; import type {ETenantType} from '../../../types/api/tenant'; import type {TenantQuery} from '../TenantPages'; -import {TenantTabsGroups, getTenantPath} from '../TenantPages'; +import {TenantTabsGroups} from '../TenantPages'; import {isDatabaseEntityType, isTopicEntityType} from '../utils/schema'; type Page = { diff --git a/src/containers/Tenant/Diagnostics/Network/Network.tsx b/src/containers/Tenant/Diagnostics/Network/Network.tsx index 7d5aeaf13a..e0ef92e59b 100644 --- a/src/containers/Tenant/Diagnostics/Network/Network.tsx +++ b/src/containers/Tenant/Diagnostics/Network/Network.tsx @@ -6,6 +6,7 @@ import {Link} from 'react-router-dom'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {Illustration} from '../../../../components/Illustration'; import {ProblemFilter} from '../../../../components/ProblemFilter'; +import {getDefaultNodePath} from '../../../../routes'; import {networkApi} from '../../../../store/reducers/network/network'; import { ProblemFilterValues, @@ -16,7 +17,6 @@ import {hideTooltip, showTooltip} from '../../../../store/reducers/tooltip'; import type {TNetNodeInfo, TNetNodePeerInfo} from '../../../../types/api/netInfo'; import {cn} from '../../../../utils/cn'; import {useAutoRefreshInterval, useTypedDispatch, useTypedSelector} from '../../../../utils/hooks'; -import {getDefaultNodePath} from '../../../Node/NodePages'; import {NodeNetwork} from './NodeNetwork/NodeNetwork'; import {getConnectedNodesCount} from './utils'; @@ -119,7 +119,7 @@ export function Network({database, databaseFullPath}: NetworkProps) { Connectivity of node{' '} {clickedNode.NodeId} {' '} diff --git a/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx b/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx index e1b2342192..1612163408 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx +++ b/src/containers/Tenant/Diagnostics/Partitions/columns/columns.tsx @@ -4,13 +4,13 @@ import DataTable from '@gravity-ui/react-data-table'; import {EntityStatus} from '../../../../../components/EntityStatus/EntityStatus'; import {MultilineTableHeader} from '../../../../../components/MultilineTableHeader/MultilineTableHeader'; import {SpeedMultiMeter} from '../../../../../components/SpeedMultiMeter'; +import {getDefaultNodePath} from '../../../../../routes'; import {useTopicDataAvailable} from '../../../../../store/reducers/capabilities/hooks'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {cn} from '../../../../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER} from '../../../../../utils/constants'; import {formatBytes, formatMsToUptime} from '../../../../../utils/dataFormatters/dataFormatters'; import {isNumeric} from '../../../../../utils/utils'; -import {getDefaultNodePath} from '../../../../Node/NodePages'; import {useDiagnosticsPageLinkGetter} from '../../DiagnosticsPages'; import { ReadLagsHeader, @@ -213,12 +213,7 @@ export const allColumns: Column[] = [ width: 200, render: ({row}) => row.partitionNodeId && row.partitionHost ? ( - + ) : ( EMPTY_DATA_PLACEHOLDER ), @@ -234,18 +229,29 @@ export const allColumns: Column[] = [ width: 200, render: ({row}) => row.connectionNodeId && row.connectionHost ? ( - + ) : ( EMPTY_DATA_PLACEHOLDER ), }, ]; +interface NodeProps { + host: string; + id: string | number; +} + +function Node({host, id}: NodeProps) { + return ( + + ); +} + // Topics without consumers have partitions data with no data corresponding to consumers // These columns will be empty and should not be displayed export const generalColumns = allColumns.filter((column) => { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx index bc5e3e1468..ec8f43f898 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/MetricsTabs/MetricsTabs.tsx @@ -3,7 +3,7 @@ import {useMemo} from 'react'; import {Flex} from '@gravity-ui/uikit'; import {useLocation} from 'react-router-dom'; -import {parseQuery} from '../../../../../routes'; +import {getTenantPath, parseQuery} from '../../../../../routes'; import {TENANT_METRICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import type {TenantMetricsTab} from '../../../../../store/reducers/tenant/types'; import type { @@ -17,7 +17,7 @@ import {SHOW_NETWORK_UTILIZATION} from '../../../../../utils/constants'; import {useSetting} from '../../../../../utils/hooks'; import {calculateMetricAggregates} from '../../../../../utils/metrics'; // no direct legend formatters needed here – handled in subcomponents -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantTabsGroups} from '../../../TenantPages'; import {CpuTab} from './components/CpuTab'; import {MemoryTab} from './components/MemoryTab'; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx index 8fa2c412a1..c6b2907f0e 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantCpu/TopShards.tsx @@ -34,7 +34,6 @@ export const TopShards = ({database, databaseFullPath}: TopShardsProps) => { diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx index f91a3df7c6..521017d338 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantMemory/TenantMemory.tsx @@ -1,11 +1,12 @@ import {InfoViewer} from '../../../../../components/InfoViewer/InfoViewer'; import {ProgressWrapper} from '../../../../../components/ProgressWrapper'; +import {getTenantPath} from '../../../../../routes'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import type {TMemoryStats} from '../../../../../types/api/nodes'; import {cn} from '../../../../../utils/cn'; import {formatStorageValuesToGb} from '../../../../../utils/dataFormatters/dataFormatters'; import {useSearchQuery} from '../../../../../utils/hooks'; -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantTabsGroups} from '../../../TenantPages'; import {StatsWrapper} from '../StatsWrapper/StatsWrapper'; import {TenantDashboard} from '../TenantDashboard/TenantDashboard'; import i18n from '../i18n'; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx index 2041dd01ae..9767aebbdb 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantNetwork/TenantNetwork.tsx @@ -1,10 +1,11 @@ import {Flex} from '@gravity-ui/uikit'; +import {getTenantPath} from '../../../../../routes'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import {cn} from '../../../../../utils/cn'; import {ENABLE_NETWORK_TABLE_KEY} from '../../../../../utils/constants'; import {useSearchQuery, useSetting} from '../../../../../utils/hooks'; -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantTabsGroups} from '../../../TenantPages'; import {StatsWrapper} from '../StatsWrapper/StatsWrapper'; import i18n from '../i18n'; diff --git a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx index 287b198f0d..69204a394f 100644 --- a/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx +++ b/src/containers/Tenant/Diagnostics/TenantOverview/TenantStorage/TenantStorage.tsx @@ -3,11 +3,12 @@ import {Flex} from '@gravity-ui/uikit'; import {InfoViewer} from '../../../../../components/InfoViewer/InfoViewer'; import {LabelWithPopover} from '../../../../../components/LabelWithPopover'; import {ProgressWrapper} from '../../../../../components/ProgressWrapper'; +import {getTenantPath} from '../../../../../routes'; import {TENANT_DIAGNOSTICS_TABS_IDS} from '../../../../../store/reducers/tenant/constants'; import type {ETenantType} from '../../../../../types/api/tenant'; import {formatStorageValues} from '../../../../../utils/dataFormatters/dataFormatters'; import {useSearchQuery} from '../../../../../utils/hooks'; -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantTabsGroups} from '../../../TenantPages'; import {StatsWrapper} from '../StatsWrapper/StatsWrapper'; import {TenantDashboard} from '../TenantDashboard/TenantDashboard'; import i18n from '../i18n'; diff --git a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx index e291059fdd..17e094d021 100644 --- a/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx +++ b/src/containers/Tenant/Diagnostics/TopQueries/QueryDetails/QueryDetailsDrawerContent.tsx @@ -2,7 +2,7 @@ import React from 'react'; import {useHistory, useLocation} from 'react-router-dom'; -import {parseQuery} from '../../../../../routes'; +import {getTenantPath, parseQuery} from '../../../../../routes'; import {changeUserInput, setIsDirty} from '../../../../../store/reducers/query/query'; import { TENANT_PAGE, @@ -11,7 +11,7 @@ import { } from '../../../../../store/reducers/tenant/constants'; import type {KeyValueRow} from '../../../../../types/api/query'; import {useTypedDispatch} from '../../../../../utils/hooks'; -import {TenantTabsGroups, getTenantPath} from '../../../TenantPages'; +import {TenantTabsGroups} from '../../../TenantPages'; import {createQueryInfoItems} from '../utils'; import {NotFoundContainer} from './NotFoundContainer'; diff --git a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx index 35cc89eedc..c39a6dd8cb 100644 --- a/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx +++ b/src/containers/Tenant/Diagnostics/TopShards/TopShards.tsx @@ -143,7 +143,6 @@ export const TopShards = ({database, path, databaseFullPath}: TopShardsProps) => return ( ( - - {id} - + {id} )} /> ) : undefined, diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx index 18f9fa0d06..eb01c0b66c 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/HealthcheckIssueDetails.tsx @@ -3,9 +3,8 @@ import React from 'react'; import {Flex} from '@gravity-ui/uikit'; import {InternalLink} from '../../../../../components/InternalLink'; -import {getTabletPagePath} from '../../../../../routes'; +import {useTabletPagePath} from '../../../../../routes'; import type {IssuesTree} from '../../../../../store/reducers/healthcheckInfo/types'; -import {useTenantQueryParams} from '../../../useTenantQueryParams'; import i18n from '../../i18n'; import type {LocationFieldCompute} from './ComputeLocation'; @@ -18,7 +17,7 @@ interface HealthcheckIssueDetailsProps { } export function IssueDetails({issue}: HealthcheckIssueDetailsProps) { - const {database} = useTenantQueryParams(); + const getTabletPagePath = useTabletPagePath(); const {detailsFields, hiddenStorageFields, hiddenComputeFields} = React.useMemo(() => { const hiddenStorageFields: LocationFieldStorage[] = []; @@ -48,9 +47,7 @@ export function IssueDetails({issue}: HealthcheckIssueDetailsProps) { ( - - {id} - + {id} )} /> ) : undefined, @@ -68,7 +65,7 @@ export function IssueDetails({issue}: HealthcheckIssueDetailsProps) { hiddenComputeFields.push('tablet'); } return {detailsFields: fields, hiddenComputeFields, hiddenStorageFields}; - }, [issue, database]); + }, [issue, getTabletPagePath]); const {location} = issue; diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx index 1d61e7a21d..44ccdaff9b 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/NodeInfo.tsx @@ -1,6 +1,6 @@ import {InternalLink} from '../../../../../components/InternalLink'; +import {getDefaultNodePath} from '../../../../../routes'; import type {Location} from '../../../../../types/api/healthcheck'; -import {getDefaultNodePath} from '../../../../Node/NodePages'; import {useTenantQueryParams} from '../../../useTenantQueryParams'; import i18n from '../../i18n'; @@ -18,7 +18,7 @@ export function NodeInfo({node, title}: NodeInfoProps) { } const nodeLink = node.id ? ( - {node.id} + {node.id} ) : undefined; return ( diff --git a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx index 69c0bf7c18..1346ff8ebb 100644 --- a/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx +++ b/src/containers/Tenant/Healthcheck/components/HealthcheckIssueDetails/StorageLocation.tsx @@ -4,9 +4,8 @@ import {Flex} from '@gravity-ui/uikit'; import {isEmpty} from 'lodash'; import {InternalLink} from '../../../../../components/InternalLink'; -import {getPDiskPagePath, getVDiskPagePath} from '../../../../../routes'; +import {getPDiskPagePath, useVDiskPagePath} from '../../../../../routes'; import type {Location} from '../../../../../types/api/healthcheck'; -import {useDatabaseFromQuery} from '../../../../../utils/hooks/useDatabaseFromQuery'; import i18n from '../../i18n'; import {NodeInfo} from './NodeInfo'; @@ -99,7 +98,7 @@ function GroupInfo({location}: StorageSectionProps) { } function VDiskInfo({location}: StorageSectionProps) { - const database = useDatabaseFromQuery(); + const getVDiskPagePath = useVDiskPagePath(); const {node, pool} = location ?? {}; const {group} = pool ?? {}; const {vdisk} = group ?? {}; @@ -119,13 +118,10 @@ function VDiskInfo({location}: StorageSectionProps) { ids={ids} renderItem={(id) => ( {id} diff --git a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx index 004eb76f92..3035d20802 100644 --- a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx +++ b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx @@ -17,7 +17,7 @@ import {toFormattedSize} from '../../../components/FormattedBytes/utils'; import {InternalLink} from '../../../components/InternalLink'; import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon'; import SplitPane from '../../../components/SplitPane'; -import {createExternalUILink} from '../../../routes'; +import {createExternalUILink, getTenantPath} from '../../../routes'; import {overviewApi} from '../../../store/reducers/overview/overview'; import {TENANT_SUMMARY_TABS_IDS} from '../../../store/reducers/tenant/constants'; import {setSummaryTab} from '../../../store/reducers/tenant/tenant'; @@ -36,7 +36,7 @@ import {prepareSystemViewType} from '../../../utils/schema'; import {EntityTitle} from '../EntityTitle/EntityTitle'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {useCurrentSchema} from '../TenantContext'; -import {TENANT_INFO_TABS, TENANT_SCHEMA_TAB, TenantTabsGroups, getTenantPath} from '../TenantPages'; +import {TENANT_INFO_TABS, TENANT_SCHEMA_TAB, TenantTabsGroups} from '../TenantPages'; import {useTenantQueryParams} from '../useTenantQueryParams'; import {getSummaryControls} from '../utils/controls'; import { diff --git a/src/containers/Tenant/Query/QueryTabs/QueryTabs.tsx b/src/containers/Tenant/Query/QueryTabs/QueryTabs.tsx index b080e54403..2edb142d59 100644 --- a/src/containers/Tenant/Query/QueryTabs/QueryTabs.tsx +++ b/src/containers/Tenant/Query/QueryTabs/QueryTabs.tsx @@ -2,10 +2,10 @@ import {Tab, TabList, TabProvider} from '@gravity-ui/uikit'; import {useLocation} from 'react-router-dom'; import {InternalLink} from '../../../../components/InternalLink/InternalLink'; -import {parseQuery} from '../../../../routes'; +import {getTenantPath, parseQuery} from '../../../../routes'; import {TENANT_QUERY_TABS_ID} from '../../../../store/reducers/tenant/constants'; import type {TenantQueryTab} from '../../../../store/reducers/tenant/types'; -import {TenantTabsGroups, getTenantPath} from '../../TenantPages'; +import {TenantTabsGroups} from '../../TenantPages'; import i18n from '../i18n'; const newQuery = { diff --git a/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx b/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx index d13dae51a2..892b3f7644 100644 --- a/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx +++ b/src/containers/Tenant/TenantNavigation/useTenantNavigation.tsx @@ -1,13 +1,12 @@ import React from 'react'; import {Pulse, Terminal} from '@gravity-ui/icons'; -import {useHistory, useLocation} from 'react-router-dom'; +import {useHistory, useLocation, useRouteMatch} from 'react-router-dom'; -import routes, {parseQuery} from '../../../routes'; +import routes, {getTenantPath, parseQuery} from '../../../routes'; import {TENANT_PAGE, TENANT_PAGES_IDS} from '../../../store/reducers/tenant/constants'; import {TENANT_INITIAL_PAGE_KEY} from '../../../utils/constants'; import {useSetting, useTypedSelector} from '../../../utils/hooks'; -import {getTenantPath} from '../TenantPages'; import i18n from '../i18n'; type TenantPages = keyof typeof TENANT_PAGES_IDS; @@ -24,12 +23,13 @@ export function useTenantNavigation() { const location = useLocation(); const queryParams = parseQuery(location); + const match = useRouteMatch(routes.tenant); const [, setInitialTenantPage] = useSetting(TENANT_INITIAL_PAGE_KEY); const {tenantPage} = useTypedSelector((state) => state.tenant); const menuItems = React.useMemo(() => { - if (location.pathname !== routes.tenant) { + if (!match) { return []; } @@ -53,7 +53,7 @@ export function useTenantNavigation() { }); return items; - }, [tenantPage, setInitialTenantPage, location.pathname, history, queryParams]); + }, [tenantPage, setInitialTenantPage, match, history, queryParams]); return menuItems; } diff --git a/src/containers/Tenant/TenantPages.tsx b/src/containers/Tenant/TenantPages.tsx index 181ae0240b..d6b0767ae5 100644 --- a/src/containers/Tenant/TenantPages.tsx +++ b/src/containers/Tenant/TenantPages.tsx @@ -1,5 +1,3 @@ -import type {CreateHrefOptions} from '../../routes'; -import routes, {createHref} from '../../routes'; import {TENANT_SUMMARY_TABS_IDS} from '../../store/reducers/tenant/constants'; import type {paramSetup} from '../../store/state-url-mapping'; import type {ExtractType} from '../../types/common'; @@ -41,7 +39,3 @@ export const TENANT_SCHEMA_TAB = [ title: 'Schema', }, ]; - -export const getTenantPath = (query: TenantQuery, options?: CreateHrefOptions) => { - return createHref(routes.tenant, undefined, query, options); -}; diff --git a/src/containers/VDiskPage/VDiskPage.tsx b/src/containers/VDiskPage/VDiskPage.tsx index 77a1285e1a..fe1ab4bd21 100644 --- a/src/containers/VDiskPage/VDiskPage.tsx +++ b/src/containers/VDiskPage/VDiskPage.tsx @@ -15,7 +15,7 @@ import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewer import {InternalLink} from '../../components/InternalLink/InternalLink'; import {PageMetaWithAutorefresh} from '../../components/PageMeta/PageMeta'; import {VDiskInfo} from '../../components/VDiskInfo/VDiskInfo'; -import {getVDiskPagePath} from '../../routes'; +import {useVDiskPagePath} from '../../routes'; import {api} from '../../store/reducers/api'; import {useDiskPagesAvailable} from '../../store/reducers/capabilities/hooks'; import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; @@ -60,6 +60,7 @@ const vDiskTabSchema = z.nativeEnum(VDISK_TABS_IDS).catch(VDISK_TABS_IDS.storage export function VDiskPage() { const dispatch = useTypedDispatch(); + const getVDiskPagePath = useVDiskPagePath(); const containerRef = React.useRef(null); const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); @@ -247,16 +248,13 @@ export function VDiskPage() { {VDISK_PAGE_TABS.map(({id, title}) => { - let path: string | undefined; - if (!isNil(vDiskId)) { - path = getVDiskPagePath( - { - nodeId: nodeId?.toString(), - vDiskId: vDiskId?.toString(), - }, - {activeTab: id, database}, - ); - } + const path = getVDiskPagePath( + { + nodeId: nodeId?.toString(), + vDiskId: vDiskId?.toString(), + }, + {activeTab: id}, + ); return ( diff --git a/src/containers/VDiskPage/VDiskTablets/columns.tsx b/src/containers/VDiskPage/VDiskTablets/columns.tsx index 12773069e9..be9707a3c7 100644 --- a/src/containers/VDiskPage/VDiskTablets/columns.tsx +++ b/src/containers/VDiskPage/VDiskTablets/columns.tsx @@ -3,11 +3,10 @@ import DataTable from '@gravity-ui/react-data-table'; import {isNil} from 'lodash'; import {InternalLink} from '../../../components/InternalLink/InternalLink'; -import {getTabletPagePath} from '../../../routes'; +import {useTabletPagePath} from '../../../routes'; import type {VDiskBlobIndexItem} from '../../../types/api/vdiskBlobIndex'; import {EMPTY_DATA_PLACEHOLDER} from '../../../utils/constants'; import {formatBytes, formatNumber} from '../../../utils/dataFormatters/dataFormatters'; -import {useDatabaseFromQuery} from '../../../utils/hooks/useDatabaseFromQuery'; import {safeParseNumber} from '../../../utils/utils'; import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants'; @@ -64,6 +63,6 @@ export function getColumns(): Column[] { } function TabletId({id}: {id: string | number}) { - const database = useDatabaseFromQuery(); - return {id}; + const getTabletPagePath = useTabletPagePath(); + return {id}; } diff --git a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx index aae87329be..99a02882d9 100644 --- a/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx +++ b/src/containers/Versions/NodesTreeTitle/NodesTreeTitle.tsx @@ -5,10 +5,10 @@ import {Button, ClipboardButton, Flex, Icon, Text} from '@gravity-ui/uikit'; import {useHistory} from 'react-router-dom'; import {VersionsBar} from '../../../components/VersionsBar/VersionsBar'; +import {getTenantPath} from '../../../routes'; import {cn} from '../../../utils/cn'; import type {PreparedNodeSystemState} from '../../../utils/nodes'; import type {PreparedVersion} from '../../../utils/versions/types'; -import {getTenantPath} from '../../Tenant/TenantPages'; import i18n from '../i18n'; import type {GroupedNodesItem} from '../types'; diff --git a/src/routes.ts b/src/routes.ts index 73baf6d7e4..b94f0a7874 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -1,3 +1,5 @@ +import * as React from 'react'; + import type {Location} from 'history'; import isEmpty from 'lodash/isEmpty'; import {compile} from 'path-to-regexp'; @@ -5,8 +7,12 @@ import qs from 'qs'; import type {QueryParamConfig} from 'use-query-params'; import {StringParam} from 'use-query-params'; -import {backend, basename, clusterName, webVersion} from './store'; +import type {ClusterTab} from './containers/Cluster/utils'; +import type {NodePageQuery, NodeTab} from './containers/Node/NodePages'; +import type {TenantQuery} from './containers/Tenant/TenantPages'; +import {backend, basename, clusterName, environment, webVersion} from './store'; import {normalizePathSlashes} from './utils'; +import {useDatabaseFromQuery} from './utils/hooks/useDatabaseFromQuery'; export const CLUSTERS = 'clusters'; export const CLUSTER = 'cluster'; @@ -19,14 +25,14 @@ export const TABLET = 'tablet'; const routes = { clusters: `/${CLUSTERS}`, - cluster: `/${CLUSTER}/:activeTab?`, - tenant: `/${TENANT}`, - node: `/${NODE}/:id/:activeTab?`, - pDisk: `/${PDISK}`, - vDisk: `/${VDISK}`, - storageGroup: `/${STORAGE_GROUP}`, - tablet: `/${TABLET}/:id`, - auth: `/auth`, + cluster: `/:environment?/${CLUSTER}/:activeTab?`, + tenant: `/:environment?/${TENANT}`, + node: `/:environment?/${NODE}/:id/:activeTab?`, + pDisk: `/:environment?/${PDISK}`, + vDisk: `/:environment?/${VDISK}`, + storageGroup: `/:environment?/${STORAGE_GROUP}`, + tablet: `/:environment?/${TABLET}/:id`, + auth: `/:environment?/auth`, } as const; export default routes; @@ -67,6 +73,7 @@ export function createHref( options: CreateHrefOptions = {}, ) { let extendedQuery = query; + let extendedParams = params ?? {}; const isBackendInQuery = 'backend' in query && Boolean(query.backend); if (backend && !isBackendInQuery && webVersion) { @@ -78,13 +85,20 @@ export function createHref( extendedQuery = {...extendedQuery, clusterName}; } + //if {environment: ""} in params - it meant we want to reset it + const isEnvironmentInParams = typeof params?.environment === 'string'; + + if (webVersion && environment && !isEnvironmentInParams) { + extendedParams = {...extendedParams, environment}; + } + const search = isEmpty(extendedQuery) ? '' : `?${qs.stringify(extendedQuery, {encode: false, arrayFormat: 'repeat'})}`; const preparedRoute = prepareRoute(route); - const compiledRoute = `${compile(preparedRoute)(params)}${search}`; + const compiledRoute = `${compile(preparedRoute)(extendedParams)}${search}`; if (options.withBasename && basename) { // For SPA links react-router adds basename itself @@ -120,20 +134,30 @@ export function getPDiskPagePath( return createHref(routes.pDisk, undefined, {...query, nodeId, pDiskId}); } -export function getVDiskPagePath( - params: { - nodeId: string | number | undefined; - vDiskId: string; - }, - query: {database: string | undefined; activeTab?: string} = {database: undefined}, -) { - return createHref(routes.vDisk, undefined, {...query, ...params}); -} - export function getStorageGroupPath(groupId: string | number, query: Query = {}) { return createHref(routes.storageGroup, undefined, {...query, groupId}); } +export function getDefaultNodePath( + params: {id: string | number; activeTab?: NodeTab}, + query: NodePageQuery = {}, + options?: CreateHrefOptions, +) { + return createHref(routes.node, params, query, options); +} + +export const getClusterPath = ( + params?: {activeTab?: ClusterTab; environment?: string}, + query = {}, + options?: CreateHrefOptions, +) => { + return createHref(routes.cluster, params, query, options); +}; + +export const getTenantPath = (query: TenantQuery, options?: CreateHrefOptions) => { + return createHref(routes.tenant, undefined, query, options); +}; + export const tabletPageQueryParams = { database: StringParam, clusterName: StringParam, @@ -143,8 +167,46 @@ export const tabletPageQueryParams = { type TabletPageQuery = QueryParamsTypeFromQueryObject; -export function getTabletPagePath(tabletId: string | number, query: TabletPageQuery = {}) { - return createHref(routes.tablet, {id: tabletId}, {...query}); +export function useVDiskPagePath() { + const database = useDatabaseFromQuery(); + + return React.useCallback( + ( + params: { + nodeId: string | number | undefined; + vDiskId: string | undefined; + }, + query: {activeTab?: string} = {}, + ) => { + if (!params.vDiskId) { + return undefined; + } + return createHref(routes.vDisk, undefined, {...query, ...params, database}); + }, + [database], + ); +} + +export function useStorageGroupPath() { + const database = useDatabaseFromQuery(); + + return React.useCallback( + (groupId: string | number, query: Query = {}) => { + return createHref(routes.storageGroup, undefined, {...query, groupId, database}); + }, + [database], + ); +} + +export function useTabletPagePath() { + const database = useDatabaseFromQuery(); + + return React.useCallback( + (tabletId: string | number, query: TabletPageQuery = {}) => { + return createHref(routes.tablet, {id: tabletId}, {...query, database}); + }, + [database], + ); } export function checkIsClustersPage(pathname: string) { diff --git a/src/services/api/meta.ts b/src/services/api/meta.ts index 15fcbc68ca..7b4096cd22 100644 --- a/src/services/api/meta.ts +++ b/src/services/api/meta.ts @@ -1,6 +1,6 @@ import type {AxiosWrapperOptions} from '@gravity-ui/axios-wrapper'; -import {metaBackend as META_BACKEND} from '../../store'; +import {environment as ENVIRONMENT, metaBackend as META_BACKEND} from '../../store'; import type {MetaCapabilitiesResponse} from '../../types/api/capabilities'; import type { MetaBaseClusterInfo, @@ -24,7 +24,8 @@ export class MetaAPI extends BaseYdbAPI { } getPath(path: string, clusterName?: string) { if (this.proxyMeta && clusterName) { - return `${META_BACKEND}/proxy/cluster/${clusterName}${path}`; + const envPrefix = ENVIRONMENT ? `/${ENVIRONMENT}` : ''; + return `${envPrefix}${META_BACKEND}/proxy/cluster/${clusterName}${path}`; } return `${META_BACKEND ?? ''}${path}`; } diff --git a/src/store/__test__/getUrlData.test.ts b/src/store/__test__/getUrlData.test.ts index 39f4506b10..53907673ef 100644 --- a/src/store/__test__/getUrlData.test.ts +++ b/src/store/__test__/getUrlData.test.ts @@ -44,7 +44,7 @@ describe('getUrlData', () => { clusterName: 'my_cluster', }); }); - test('should parse pathname with folder and some prefix', () => { + test('should parse pathname without folder', () => { windowSpy.mockImplementation(() => { return { location: { @@ -60,6 +60,91 @@ describe('getUrlData', () => { clusterName: 'my_cluster', }); }); + test('should extract environment from first segment', () => { + windowSpy.mockImplementation(() => { + return { + location: { + href: 'http://ydb-ui/cloud-prod/cluster?clusterName=my_cluster&backend=http://my-node:8765', + pathname: + '/cloud-prod/cluster?clusterName=my_cluster&backend=http://my-node:8765', + }, + } as Window & typeof globalThis; + }); + const result = getUrlData({ + singleClusterMode: false, + customBackend: undefined, + allowedEnvironments: ['cloud-prod', 'cloud-preprod'], + }); + expect(result).toEqual({ + basename: '', + backend: 'http://my-node:8765', + clusterName: 'my_cluster', + environment: 'cloud-prod', + }); + }); + test('should extract environment from first segment with monitoring folder', () => { + windowSpy.mockImplementation(() => { + return { + location: { + href: 'http://ydb-ui/cloud-preprod/api/meta3/proxy/cluster/pre-prod_global/monitoring/cluster/tenants', + pathname: + '/cloud-preprod/api/meta3/proxy/cluster/pre-prod_global/monitoring/cluster/tenants', + }, + } as Window & typeof globalThis; + }); + const result = getUrlData({ + singleClusterMode: false, + customBackend: undefined, + allowedEnvironments: ['cloud-prod', 'cloud-preprod'], + }); + expect(result).toEqual({ + basename: '/cloud-preprod/api/meta3/proxy/cluster/pre-prod_global/monitoring', + backend: undefined, + clusterName: undefined, + environment: 'cloud-preprod', + }); + }); + test('should not extract environment if not in allowed list', () => { + windowSpy.mockImplementation(() => { + return { + location: { + href: 'http://ydb-ui/cluster/tenants?clusterName=my_cluster', + pathname: '/cluster/tenants?clusterName=my_cluster', + }, + } as Window & typeof globalThis; + }); + const result = getUrlData({ + singleClusterMode: false, + customBackend: undefined, + allowedEnvironments: ['cloud-prod', 'cloud-preprod'], + }); + expect(result).toEqual({ + basename: '', + backend: undefined, + clusterName: 'my_cluster', + environment: undefined, + }); + }); + test('should not extract environment without allowedEnvironments list', () => { + windowSpy.mockImplementation(() => { + return { + location: { + href: 'http://ydb-ui/my-env/cluster?clusterName=my_cluster', + pathname: '/my-env/cluster?clusterName=my_cluster', + }, + } as Window & typeof globalThis; + }); + const result = getUrlData({ + singleClusterMode: false, + customBackend: undefined, + }); + expect(result).toEqual({ + basename: '', + backend: undefined, + clusterName: 'my_cluster', + environment: undefined, + }); + }); }); describe('single-cluster version with custom backend', () => { test('should parse correclty parse pathname', () => { diff --git a/src/store/configureStore.ts b/src/store/configureStore.ts index 820e7df1da..c29d078f3e 100644 --- a/src/store/configureStore.ts +++ b/src/store/configureStore.ts @@ -13,7 +13,10 @@ import {syncUserSettingsFromLS} from './reducers/settings/settings'; import {UPDATE_REF} from './reducers/tooltip'; import getLocationMiddleware from './state-url-mapping'; -export let backend: string | undefined, basename: string, clusterName: string | undefined; +export let backend: string | undefined, + basename: string, + clusterName: string | undefined, + environment: string | undefined; function _configureStore< S = any, @@ -57,6 +60,7 @@ const isSingleClusterMode = `${metaBackend}` === 'undefined'; export function configureStore({ aRootReducer = rootReducer, singleClusterMode = isSingleClusterMode, + environments = [] as string[], api = new YdbEmbeddedAPI({ webVersion, singleClusterMode: isSingleClusterMode, @@ -67,8 +71,12 @@ export function configureStore({ defaults: undefined, }), } = {}) { - const params = getUrlData({singleClusterMode, customBackend}); - ({basename, clusterName} = params); + const params = getUrlData({ + singleClusterMode, + customBackend, + allowedEnvironments: environments, + }); + ({basename, clusterName, environment} = params); backend = params.backend; const history = createBrowserHistory({basename}); diff --git a/src/store/getUrlData.ts b/src/store/getUrlData.ts index fa704f6cfc..3422d206d6 100644 --- a/src/store/getUrlData.ts +++ b/src/store/getUrlData.ts @@ -3,9 +3,11 @@ import {normalizePathSlashes} from '../utils'; export const getUrlData = ({ singleClusterMode, customBackend, + allowedEnvironments, }: { singleClusterMode: boolean; customBackend?: string; + allowedEnvironments?: string[]; }) => { // UI could be located in "monitoring" or "ui" folders // my-host:8765/some/path/monitoring/react-router-path or my-host:8765/some/path/ui/react-router-path @@ -28,10 +30,18 @@ export const getUrlData = ({ if (!singleClusterMode) { // Multi-cluster version // Cluster and backend are determined by url params + // Extract environment from the first path segment if it's in allowedEnvironments list + // e.g., /cloud-preprod/api/meta3/proxy/cluster/pre-prod_global/monitoring/cluster -> environment: 'cloud-preprod' + // e.g., /cloud-prod/cluster -> environment: 'cloud-prod' + // Environment is only extracted if allowedEnvironments list is provided + const firstSegment = window.location.pathname.split('/').filter(Boolean)[0]; + const environment = allowedEnvironments?.includes(firstSegment) ? firstSegment : undefined; + return { basename, backend, clusterName, + environment, }; } else if (customBackend) { // Single-cluster version @@ -39,7 +49,7 @@ export const getUrlData = ({ // There is a backend url param for requests return { basename, - backend: backend ? backend : customBackend, + backend: backend || customBackend, }; } else { // Single-cluster version diff --git a/src/store/index.ts b/src/store/index.ts index dfd8a89193..e868f44cd2 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,5 +1,6 @@ export { backend, + environment, basename, clusterName, configureStore, diff --git a/src/store/reducers/clusters/types.ts b/src/store/reducers/clusters/types.ts index 0acb621278..8055210760 100644 --- a/src/store/reducers/clusters/types.ts +++ b/src/store/reducers/clusters/types.ts @@ -1,9 +1,10 @@ -import type {MetaExtendedClusterInfo} from '../../../types/api/meta'; +import type {MetaClusterSettings, MetaExtendedClusterInfo} from '../../../types/api/meta'; import type {PreparedVersion} from '../../../utils/versions/types'; -export interface PreparedCluster extends MetaExtendedClusterInfo { +export interface PreparedCluster extends Omit { preparedVersions: PreparedVersion[]; preparedBackend?: string; + settings?: MetaClusterSettings; } export interface ClustersFilters { diff --git a/src/store/reducers/header/types.ts b/src/store/reducers/header/types.ts index d3e89a7fc4..79b2ff9bcf 100644 --- a/src/store/reducers/header/types.ts +++ b/src/store/reducers/header/types.ts @@ -20,6 +20,7 @@ export interface ClustersBreadcrumbsOptions {} export interface ClusterBreadcrumbsOptions extends ClustersBreadcrumbsOptions { clusterName?: string; clusterTab?: ClusterTab; + environment?: string; } export interface TenantBreadcrumbsOptions extends ClusterBreadcrumbsOptions { diff --git a/src/store/state-url-mapping.ts b/src/store/state-url-mapping.ts index e4cce738d3..8aea912df4 100644 --- a/src/store/state-url-mapping.ts +++ b/src/store/state-url-mapping.ts @@ -72,12 +72,68 @@ export const paramSetup = { stateKey: 'partitions.selectedConsumer', }, }, + '/*/tenant': { + sort: { + stateKey: 'heatmap.sort', + initialState: initialHeatmapState.sort, + type: 'bool', + }, + heatmap: { + stateKey: 'heatmap.heatmap', + initialState: initialHeatmapState.heatmap, + type: 'bool', + }, + currentMetric: { + stateKey: 'heatmap.currentMetric', + initialState: initialHeatmapState.currentMetric, + }, + tenantPage: { + stateKey: 'tenant.tenantPage', + }, + queryTab: { + stateKey: 'tenant.queryTab', + }, + diagnosticsTab: { + stateKey: 'tenant.diagnosticsTab', + }, + summaryTab: { + stateKey: 'tenant.summaryTab', + }, + metricsTab: { + stateKey: 'tenant.metricsTab', + initialState: initialTenantState.metricsTab, + }, + shardsMode: { + stateKey: 'shardsWorkload.mode', + }, + shardsDateFrom: { + stateKey: 'shardsWorkload.from', + }, + shardsDateTo: { + stateKey: 'shardsWorkload.to', + }, + topQueriesDateFrom: { + stateKey: 'executeTopQueries.from', + }, + topQueriesDateTo: { + stateKey: 'executeTopQueries.to', + }, + selectedConsumer: { + stateKey: 'partitions.selectedConsumer', + }, + }, '/cluster/tenants': { search: { stateKey: 'tenants.searchValue', initialState: '', }, }, + '/*/cluster/tenants': { + search: { + stateKey: 'tenants.searchValue', + initialState: '', + }, + }, } as const; function mergeLocationToState(state: S, location: Pick): S { diff --git a/src/types/api/meta.ts b/src/types/api/meta.ts index ba17091a2c..87974ee8c1 100644 --- a/src/types/api/meta.ts +++ b/src/types/api/meta.ts @@ -97,4 +97,5 @@ export interface MetaClusterTraceCheck { export interface MetaClusterSettings { use_meta_proxy?: boolean; + auth_service?: string; } diff --git a/src/utils/parseBalancer.ts b/src/utils/parseBalancer.ts index c8b9767a34..6f74ee66a0 100644 --- a/src/utils/parseBalancer.ts +++ b/src/utils/parseBalancer.ts @@ -1,3 +1,5 @@ +import {environment} from '../store'; + import {normalizePathSlashes} from '.'; const protocolRegex = /^http[s]?:\/\//; @@ -65,7 +67,16 @@ export function prepareBackendFromBalancer(rawBalancer: string) { // Use meta_backend if it is defined to form backend url if (window.meta_backend) { - return normalizePathSlashes(`${window.meta_backend}/${preparedBalancer}`); + const metaBackend = window.meta_backend; + const envPrefix = environment ? `/${environment}` : ''; + + // If meta_backend is a full URL (has protocol), don't add environment prefix + if (protocolRegex.test(metaBackend)) { + return normalizePathSlashes(`${metaBackend}/${preparedBalancer}`); + } + + // For relative meta_backend, include environment prefix + return normalizePathSlashes(`${envPrefix}/${metaBackend}/${preparedBalancer}`); } return preparedBalancer;