Skip to content

feat(Cluster): add Network tab #2424

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions src/components/NetworkTable/NetworkTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Nodes} from '../../containers/Nodes/Nodes';
import type {NodesProps} from '../../containers/Nodes/Nodes';

import {getNetworkTableNodesColumns} from './columns';
import {
NETWORK_DEFAULT_NODES_COLUMNS,
NETWORK_NODES_GROUP_BY_PARAMS,
NETWORK_NODES_TABLE_SELECTED_COLUMNS_KEY,
NETWORK_REQUIRED_NODES_COLUMNS,
} from './constants';

type NetworkWrapperProps = Pick<
NodesProps,
'path' | 'scrollContainerRef' | 'additionalNodesProps' | 'database'
>;

export function NetworkTable({
database,
path,
scrollContainerRef,
additionalNodesProps,
}: NetworkWrapperProps) {
return (
<Nodes
path={path}
database={database}
scrollContainerRef={scrollContainerRef}
withPeerRoleFilter={Boolean(database)}
additionalNodesProps={additionalNodesProps}
columns={getNetworkTableNodesColumns({
database: database,
getNodeRef: additionalNodesProps?.getNodeRef,
})}
defaultColumnsIds={NETWORK_DEFAULT_NODES_COLUMNS}
requiredColumnsIds={NETWORK_REQUIRED_NODES_COLUMNS}
selectedColumnsKey={NETWORK_NODES_TABLE_SELECTED_COLUMNS_KEY}
groupByParams={NETWORK_NODES_GROUP_BY_PARAMS}
/>
);
}
41 changes: 41 additions & 0 deletions src/components/NetworkTable/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type {NodesPreparedEntity} from '../../store/reducers/nodes/types';
import type {Column} from '../../utils/tableUtils/types';
import {
getClockSkewColumn,
getConnectionsColumn,
getCpuColumn,
getDataCenterColumn,
getHostColumn,
getNetworkUtilizationColumn,
getNodeIdColumn,
getPingTimeColumn,
getPoolsColumn,
getRackColumn,
getReceiveThroughputColumn,
getSendThroughputColumn,
getUptimeColumn,
} from '../nodesColumns/columns';
import {isSortableNodesColumn} from '../nodesColumns/constants';
import type {GetNodesColumnsParams} from '../nodesColumns/types';

export function getNetworkTableNodesColumns(params: GetNodesColumnsParams) {
const columns: Column<NodesPreparedEntity>[] = [
getNodeIdColumn(),
getHostColumn(params, {statusForIcon: 'ConnectStatus'}),
getDataCenterColumn(),
getRackColumn(),
getUptimeColumn(),
getCpuColumn(),
getPoolsColumn(),
getConnectionsColumn(),
getNetworkUtilizationColumn(),
getSendThroughputColumn(),
getReceiveThroughputColumn(),
getPingTimeColumn(),
getClockSkewColumn(),
];

return columns.map((column) => {
return {...column, sortable: isSortableNodesColumn(column.name)};
});
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {NodesColumnId} from '../../../../../components/nodesColumns/constants';
import type {NodesGroupByField} from '../../../../../types/api/nodes';
import type {NodesGroupByField} from '../../types/api/nodes';
import type {NodesColumnId} from '../nodesColumns/constants';

export const NETWORK_NODES_TABLE_SELECTED_COLUMNS_KEY = 'networkNodesTableSelectedColumns';

Expand Down
20 changes: 20 additions & 0 deletions src/components/NetworkTable/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {
useNodesHandlerHasWorkingClusterNetworkStats,
useViewerNodesHandlerHasNetworkStats,
} from '../../store/reducers/capabilities/hooks';
import {ENABLE_NETWORK_TABLE_KEY} from '../../utils/constants';
import {useSetting} from '../../utils/hooks';

export function useShouldShowDatabaseNetworkTable() {
const viewerNodesHasNetworkStats = useViewerNodesHandlerHasNetworkStats();
const [networkTableEnabled] = useSetting(ENABLE_NETWORK_TABLE_KEY);

return Boolean(viewerNodesHasNetworkStats && networkTableEnabled);
}

export function useShouldShowClusterNetworkTable() {
const nodesHasWorkingClusterNetworkStats = useNodesHandlerHasWorkingClusterNetworkStats();
const [networkTableEnabled] = useSetting(ENABLE_NETWORK_TABLE_KEY);

return Boolean(nodesHasWorkingClusterNetworkStats && networkTableEnabled);
}
39 changes: 35 additions & 4 deletions src/containers/Cluster/Cluster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import {AutoRefreshControl} from '../../components/AutoRefreshControl/AutoRefres
import {EntityStatus} from '../../components/EntityStatusNew/EntityStatus';
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 {useClusterDashboardAvailable} from '../../store/reducers/capabilities/hooks';
import {
INITIAL_DEFAULT_CLUSTER_TAB,
clusterApi,
selectClusterTabletsWithFqdn,
selectClusterTitle,
Expand Down Expand Up @@ -55,6 +58,8 @@ export function Cluster({
const container = React.useRef<HTMLDivElement>(null);
const isClusterDashboardAvailable = useClusterDashboardAvailable();

const shouldShowNetworkTable = useShouldShowClusterNetworkTable();

const [autoRefreshInterval] = useAutoRefreshInterval();

const dispatch = useTypedDispatch();
Expand Down Expand Up @@ -92,6 +97,14 @@ export function Cluster({
dispatch(setHeaderBreadcrumbs('cluster', {}));
}, [dispatch]);

const actualClusterTabs = React.useMemo(() => {
if (shouldShowNetworkTable) {
return clusterTabs;
} else {
return clusterTabs.filter((tab) => tab.id !== clusterTabsIds.network);
}
}, [shouldShowNetworkTable]);

const getClusterTitle = () => {
if (infoLoading) {
return <Skeleton className={b('title-skeleton')} />;
Expand All @@ -110,8 +123,8 @@ export function Cluster({
};

const activeTab = React.useMemo(
() => clusterTabs.find(({id}) => id === activeTabId),
[activeTabId],
() => actualClusterTabs.find(({id}) => id === activeTabId),
[activeTabId, actualClusterTabs],
);

return (
Expand Down Expand Up @@ -142,7 +155,7 @@ export function Cluster({
size="l"
allowNotSelected={true}
activeTab={activeTabId}
items={clusterTabs}
items={actualClusterTabs}
wrapTo={({id}, node) => {
const path = getClusterPath(id as ClusterTab, {clusterName, backend});
return (
Expand Down Expand Up @@ -202,6 +215,19 @@ export function Cluster({
>
<PaginatedStorage scrollContainerRef={container} />
</Route>
{shouldShowNetworkTable && (
<Route
path={
getLocationObjectFromHref(getClusterPath(clusterTabsIds.network))
.pathname
}
>
<NetworkTable
scrollContainerRef={container}
additionalNodesProps={additionalNodesProps}
/>
</Route>
)}
<Route
path={
getLocationObjectFromHref(getClusterPath(clusterTabsIds.versions))
Expand All @@ -226,11 +252,16 @@ function useClusterTab() {

const defaultTab = useTypedSelector((state) => state.cluster.defaultClusterTab);

const shouldShowNetworkTable = useShouldShowClusterNetworkTable();

const match = useRouteMatch<{activeTab: string}>(routes.cluster);

const {activeTab: activeTabFromParams} = match?.params || {};
let activeTab: ClusterTab;
if (isClusterTab(activeTabFromParams)) {

if (!shouldShowNetworkTable && activeTabFromParams === clusterTabsIds.network) {
activeTab = INITIAL_DEFAULT_CLUSTER_TAB;
} else if (isClusterTab(activeTabFromParams)) {
activeTab = activeTabFromParams;
} else {
activeTab = defaultTab;
Expand Down
7 changes: 6 additions & 1 deletion src/containers/Cluster/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const clusterTabsIds = {
tenants: 'tenants',
nodes: 'nodes',
storage: 'storage',
network: 'network',
versions: 'versions',
tablets: 'tablets',
} as const;
Expand All @@ -25,6 +26,10 @@ const storage = {
id: clusterTabsIds.storage,
title: 'Storage',
};
const network = {
id: clusterTabsIds.network,
title: 'Network',
};
const versions = {
id: clusterTabsIds.versions,
title: 'Versions',
Expand All @@ -34,7 +39,7 @@ const tablets = {
title: 'Tablets',
};

export const clusterTabs = [tenants, nodes, storage, tablets, versions];
export const clusterTabs = [tenants, nodes, storage, network, tablets, versions];

export function isClusterTab(tab: any): tab is ClusterTab {
return Object.values(clusterTabsIds).includes(tab);
Expand Down

This file was deleted.

33 changes: 5 additions & 28 deletions src/containers/Tenant/Diagnostics/Network/NetworkWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,10 @@
import {LoaderWrapper} from '../../../../components/LoaderWrapper/LoaderWrapper';
import {
useCapabilitiesLoaded,
useViewerNodesHandlerHasNetworkStats,
} from '../../../../store/reducers/capabilities/hooks';
import {ENABLE_NETWORK_TABLE_KEY} from '../../../../utils/constants';
import {useSetting} from '../../../../utils/hooks';
import {NetworkTable} from '../../../../components/NetworkTable/NetworkTable';
import {useShouldShowDatabaseNetworkTable} from '../../../../components/NetworkTable/hooks';
import {useCapabilitiesLoaded} from '../../../../store/reducers/capabilities/hooks';
import type {NodesProps} from '../../../Nodes/Nodes';
import {Nodes} from '../../../Nodes/Nodes';

import {Network} from './Network';
import {getNetworkTableNodesColumns} from './NetworkTable/columns';
import {
NETWORK_DEFAULT_NODES_COLUMNS,
NETWORK_NODES_GROUP_BY_PARAMS,
NETWORK_NODES_TABLE_SELECTED_COLUMNS_KEY,
NETWORK_REQUIRED_NODES_COLUMNS,
} from './NetworkTable/constants';

interface NetworkWrapperProps
extends Pick<NodesProps, 'path' | 'scrollContainerRef' | 'additionalNodesProps'> {
Expand All @@ -29,28 +18,16 @@ export function NetworkWrapper({
additionalNodesProps,
}: NetworkWrapperProps) {
const capabilitiesLoaded = useCapabilitiesLoaded();
const viewerNodesHasNetworkStats = useViewerNodesHandlerHasNetworkStats();
const [networkTableEnabled] = useSetting(ENABLE_NETWORK_TABLE_KEY);

const shouldUseNetworkNodesTable = viewerNodesHasNetworkStats && networkTableEnabled;
const shouldUseNetworkNodesTable = useShouldShowDatabaseNetworkTable();

const renderContent = () => {
if (shouldUseNetworkNodesTable) {
return (
<Nodes
<NetworkTable
path={path}
database={database}
scrollContainerRef={scrollContainerRef}
withPeerRoleFilter
additionalNodesProps={additionalNodesProps}
columns={getNetworkTableNodesColumns({
database: database,
getNodeRef: additionalNodesProps?.getNodeRef,
})}
defaultColumnsIds={NETWORK_DEFAULT_NODES_COLUMNS}
requiredColumnsIds={NETWORK_REQUIRED_NODES_COLUMNS}
selectedColumnsKey={NETWORK_NODES_TABLE_SELECTED_COLUMNS_KEY}
groupByParams={NETWORK_NODES_GROUP_BY_PARAMS}
/>
);
}
Expand Down
7 changes: 7 additions & 0 deletions src/store/reducers/capabilities/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ export const useViewerNodesHandlerHasNetworkStats = () => {
return useGetFeatureVersion('/viewer/nodes') > 13;
};

// Before this version handler has very big response size if nodes quantity is more than 100
// Response size could be up to 20-30MB, it loads very long and freezes UI
// It is not very common for databases, but an often case for clusters
export const useNodesHandlerHasWorkingClusterNetworkStats = () => {
return useGetFeatureVersion('/viewer/nodes') >= 16;
};

export const useFeatureFlagsAvailable = () => {
return useGetFeatureVersion('/viewer/feature_flags') > 1;
};
Expand Down
4 changes: 3 additions & 1 deletion src/store/reducers/cluster/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ import {
parseGroupsStatsQueryResponse,
} from './utils';

export const INITIAL_DEFAULT_CLUSTER_TAB = clusterTabsIds.tenants;

const defaultClusterTabLS = localStorage.getItem(DEFAULT_CLUSTER_TAB_KEY);

let defaultClusterTab: ClusterTab;
if (isClusterTab(defaultClusterTabLS)) {
defaultClusterTab = defaultClusterTabLS;
} else {
defaultClusterTab = clusterTabsIds.tenants;
defaultClusterTab = INITIAL_DEFAULT_CLUSTER_TAB;
}

const initialState: ClusterState = {
Expand Down
Loading