From 47089552ee6e9ea0f2227221042f5956efc97e0f Mon Sep 17 00:00:00 2001 From: mufazalov Date: Tue, 14 Jan 2025 19:03:49 +0300 Subject: [PATCH] feat(Storage): group disks by DC --- src/components/PDiskPopup/PDiskPopup.tsx | 19 ++++++----- src/components/VDiskPopup/VDiskPopup.tsx | 10 +++--- src/containers/Storage/Disks/Disks.scss | 20 ++++++++++- src/containers/Storage/Disks/Disks.tsx | 23 +++++++++---- .../columns/StorageGroupsColumns.scss | 19 ----------- .../Storage/StorageGroups/columns/columns.tsx | 17 ++-------- src/containers/Storage/VDisks/VDisks.scss | 29 ++++++++++++++++ src/containers/Storage/VDisks/VDisks.tsx | 33 +++++++++++++++++++ src/containers/Storage/utils/index.ts | 24 ++++++++++++++ .../Diagnostics/Partitions/Partitions.tsx | 4 +-- .../Diagnostics/Partitions/utils/index.ts | 8 ++--- src/store/reducers/cluster/cluster.ts | 7 ++-- src/store/reducers/nodesList.ts | 6 ++-- src/store/reducers/tablet.ts | 6 ++-- src/store/reducers/tablets.ts | 7 ++-- src/types/store/nodesList.ts | 8 ++++- src/utils/nodes.ts | 13 +++++--- 17 files changed, 175 insertions(+), 78 deletions(-) create mode 100644 src/containers/Storage/VDisks/VDisks.scss create mode 100644 src/containers/Storage/VDisks/VDisks.tsx diff --git a/src/components/PDiskPopup/PDiskPopup.tsx b/src/components/PDiskPopup/PDiskPopup.tsx index b8827359f..502ca1aeb 100644 --- a/src/components/PDiskPopup/PDiskPopup.tsx +++ b/src/components/PDiskPopup/PDiskPopup.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; -import {selectNodeHostsMap} from '../../store/reducers/nodesList'; +import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; import {valueIsDefined} from '../../utils'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; @@ -17,7 +17,7 @@ const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow]; export const preparePDiskData = ( data: PreparedPDisk, - nodeHost?: string, + nodeData?: {Host?: string; DC?: string}, withDeveloperUILink?: boolean, ) => { const { @@ -46,8 +46,11 @@ export const preparePDiskData = ( pdiskData.push({label: 'Node Id', value: NodeId}); } - if (nodeHost) { - pdiskData.push({label: 'Host', value: nodeHost}); + if (nodeData?.Host) { + pdiskData.push({label: 'Host', value: nodeData.Host}); + } + if (nodeData?.DC) { + pdiskData.push({label: 'DC', value: nodeData.DC}); } if (Path) { @@ -90,11 +93,11 @@ interface PDiskPopupProps { export const PDiskPopup = ({data}: PDiskPopupProps) => { const isUserAllowedToMakeChanges = useTypedSelector(selectIsUserAllowedToMakeChanges); - const nodeHostsMap = useTypedSelector(selectNodeHostsMap); - const nodeHost = valueIsDefined(data.NodeId) ? nodeHostsMap?.get(data.NodeId) : undefined; + const nodesMap = useTypedSelector(selectNodesMap); + const nodeData = valueIsDefined(data.NodeId) ? nodesMap?.get(data.NodeId) : undefined; const info = React.useMemo( - () => preparePDiskData(data, nodeHost, isUserAllowedToMakeChanges), - [data, nodeHost, isUserAllowedToMakeChanges], + () => preparePDiskData(data, nodeData, isUserAllowedToMakeChanges), + [data, nodeData, isUserAllowedToMakeChanges], ); return ; diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 8181bf015..e4d32c534 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -3,7 +3,7 @@ import React from 'react'; import {Label} from '@gravity-ui/uikit'; import {selectIsUserAllowedToMakeChanges} from '../../store/reducers/authentication/authentication'; -import {selectNodeHostsMap} from '../../store/reducers/nodesList'; +import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; import {valueIsDefined} from '../../utils'; import {cn} from '../../utils/cn'; @@ -188,14 +188,14 @@ export const VDiskPopup = ({data}: VDiskPopupProps) => { [data, isFullData, isUserAllowedToMakeChanges], ); - const nodeHostsMap = useTypedSelector(selectNodeHostsMap); - const nodeHost = valueIsDefined(data.NodeId) ? nodeHostsMap?.get(data.NodeId) : undefined; + const nodesMap = useTypedSelector(selectNodesMap); + const nodeData = valueIsDefined(data.NodeId) ? nodesMap?.get(data.NodeId) : undefined; const pdiskInfo = React.useMemo( () => isFullData && data.PDisk && - preparePDiskData(data.PDisk, nodeHost, isUserAllowedToMakeChanges), - [data, nodeHost, isFullData, isUserAllowedToMakeChanges], + preparePDiskData(data.PDisk, nodeData, isUserAllowedToMakeChanges), + [data, nodeData, isFullData, isUserAllowedToMakeChanges], ); const donorsInfo: InfoViewerItem[] = []; diff --git a/src/containers/Storage/Disks/Disks.scss b/src/containers/Storage/Disks/Disks.scss index 75c8f9d99..d18c288e5 100644 --- a/src/containers/Storage/Disks/Disks.scss +++ b/src/containers/Storage/Disks/Disks.scss @@ -10,7 +10,6 @@ display: flex; flex-direction: row; justify-content: left; - gap: 6px; width: max-content; } @@ -18,6 +17,16 @@ &__vdisk-item { flex-basis: 8px; flex-shrink: 0; + + margin-right: 4px; + + &_with-dc-margin { + margin-right: 16px; + } + + &:last-child { + margin-right: 0; + } } &__vdisk-progress-bar { --progress-bar-compact-height: 18px; @@ -27,6 +36,15 @@ &__pdisk-item { min-width: 80px; + margin-right: 6px; + + &_with-dc-margin { + margin-right: 20px; + } + + &:last-child { + margin-right: 0; + } } &__pdisk-progress-bar { --progress-bar-full-height: 20px; diff --git a/src/containers/Storage/Disks/Disks.tsx b/src/containers/Storage/Disks/Disks.tsx index c450fc3ed..54513e685 100644 --- a/src/containers/Storage/Disks/Disks.tsx +++ b/src/containers/Storage/Disks/Disks.tsx @@ -8,7 +8,7 @@ import {cn} from '../../../utils/cn'; import type {PreparedVDisk} from '../../../utils/disks/types'; import {PDisk} from '../PDisk'; import type {StorageViewContext} from '../types'; -import {isVdiskActive} from '../utils'; +import {isVdiskActive, useVDisksWithDCMargins} from '../utils'; import './Disks.scss'; @@ -24,6 +24,8 @@ interface DisksProps { export function Disks({vDisks = [], viewContext}: DisksProps) { const [highlightedVDisk, setHighlightedVDisk] = React.useState(); + const vDisksWithDCMargins = useVDisksWithDCMargins(vDisks); + const { theme: {spaceBaseSize}, } = useLayoutContext(); @@ -37,8 +39,8 @@ export function Disks({vDisks = [], viewContext}: DisksProps) { return (
- - {vDisks?.map((vDisk) => ( + + {vDisks?.map((vDisk, index) => ( ))}
- {vDisks?.map((vDisk) => ( + {vDisks?.map((vDisk, index) => ( ))}
@@ -70,6 +74,7 @@ interface DisksItemProps { highlightedVDisk: string | undefined; setHighlightedVDisk: (id: string | undefined) => void; unavailableVDiskWidth?: number; + withDCMargin?: boolean; } function VDiskItem({ @@ -78,6 +83,7 @@ function VDiskItem({ inactive, setHighlightedVDisk, unavailableVDiskWidth, + withDCMargin, }: DisksItemProps) { // Do not show PDisk popup for VDisk const vDiskToShow = {...vDisk, PDisk: undefined}; @@ -89,7 +95,10 @@ function VDiskItem({ const flexGrow = Number(vDiskToShow.AllocatedSize) || 1; return ( -
+
( name: STORAGE_GROUPS_COLUMNS_IDS.VDisks, header: STORAGE_GROUPS_COLUMNS_TITLES.VDisks, className: b('vdisks-column'), - render: ({row}) => ( -
- {row.VDisks?.map((vDisk) => ( - - ))} -
- ), + render: ({row}) => , align: DataTable.CENTER, width: 900, resizeable: false, diff --git a/src/containers/Storage/VDisks/VDisks.scss b/src/containers/Storage/VDisks/VDisks.scss new file mode 100644 index 000000000..95e61ad21 --- /dev/null +++ b/src/containers/Storage/VDisks/VDisks.scss @@ -0,0 +1,29 @@ +.ydb-storage-vdisks { + &__wrapper { + display: flex; + justify-content: center; + + min-width: 500px; + } + + &__item { + flex-grow: 1; + + max-width: 200px; + margin-right: 10px; + + &_with-dc-margin { + margin-right: 20px; + } + + &:last-child { + margin-right: 0px; + } + + .stack__layer { + .data-table__row:hover & { + background: var(--ydb-data-table-color-hover); + } + } + } +} diff --git a/src/containers/Storage/VDisks/VDisks.tsx b/src/containers/Storage/VDisks/VDisks.tsx new file mode 100644 index 000000000..221613ff7 --- /dev/null +++ b/src/containers/Storage/VDisks/VDisks.tsx @@ -0,0 +1,33 @@ +import {VDiskWithDonorsStack} from '../../../components/VDisk/VDiskWithDonorsStack'; +import {cn} from '../../../utils/cn'; +import type {PreparedVDisk} from '../../../utils/disks/types'; +import type {StorageViewContext} from '../types'; +import {isVdiskActive, useVDisksWithDCMargins} from '../utils'; + +import './VDisks.scss'; + +const b = cn('ydb-storage-vdisks'); + +interface VDisksProps { + vDisks?: PreparedVDisk[]; + viewContext?: StorageViewContext; +} + +export function VDisks({vDisks, viewContext}: VDisksProps) { + const vDisksWithDCMargins = useVDisksWithDCMargins(vDisks); + + return ( +
+ {vDisks?.map((vDisk, index) => ( + + ))} +
+ ); +} diff --git a/src/containers/Storage/utils/index.ts b/src/containers/Storage/utils/index.ts index c09abbbca..8fdf27428 100644 --- a/src/containers/Storage/utils/index.ts +++ b/src/containers/Storage/utils/index.ts @@ -1,7 +1,11 @@ +import React from 'react'; + +import {selectNodesMap} from '../../../store/reducers/nodesList'; import type {PreparedStorageGroup} from '../../../store/reducers/storage/types'; import {valueIsDefined} from '../../../utils'; import type {PreparedVDisk} from '../../../utils/disks/types'; import {generateEvaluator} from '../../../utils/generateEvaluator'; +import {useTypedSelector} from '../../../utils/hooks'; import type {StorageViewContext} from '../types'; const defaultDegradationEvaluator = generateEvaluator(['success', 'warning', 'danger'], 1, 2); @@ -79,3 +83,23 @@ export function getStorageGroupsInitialEntitiesCount( return DEFAULT_ENTITIES_COUNT; } + +export function useVDisksWithDCMargins(vDisks: PreparedVDisk[] = []) { + const nodesMap = useTypedSelector(selectNodesMap); + + return React.useMemo(() => { + const disksWithMargins: number[] = []; + + // Backend returns disks sorted by DC, so we don't need to apply any additional sorting + vDisks.forEach((disk, index) => { + const dc1 = nodesMap?.get(Number(disk?.NodeId))?.DC; + const dc2 = nodesMap?.get(Number(vDisks[index + 1]?.NodeId))?.DC; + + if (dc1 !== dc2) { + disksWithMargins.push(index); + } + }); + + return disksWithMargins; + }, [vDisks, nodesMap]); +} diff --git a/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx b/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx index f67041ff3..c32923596 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx +++ b/src/containers/Tenant/Diagnostics/Partitions/Partitions.tsx @@ -5,7 +5,7 @@ import {skipToken} from '@reduxjs/toolkit/query'; import {ResponseError} from '../../../../components/Errors/ResponseError'; import {ResizeableDataTable} from '../../../../components/ResizeableDataTable/ResizeableDataTable'; import {TableSkeleton} from '../../../../components/TableSkeleton/TableSkeleton'; -import {nodesListApi, selectNodeHostsMap} from '../../../../store/reducers/nodesList'; +import {nodesListApi, selectNodesMap} from '../../../../store/reducers/nodesList'; import {partitionsApi, setSelectedConsumer} from '../../../../store/reducers/partitions/partitions'; import {selectConsumersNames, topicApi} from '../../../../store/reducers/topic'; import {cn} from '../../../../utils/cn'; @@ -55,7 +55,7 @@ export const Partitions = ({path, database}: PartitionsProps) => { error: nodesError, } = nodesListApi.useGetNodesListQuery(undefined); const nodesLoading = nodesIsFetching && nodesData === undefined; - const nodeHostsMap = useTypedSelector(selectNodeHostsMap); + const nodeHostsMap = useTypedSelector(selectNodesMap); const [hiddenColumns, setHiddenColumns] = useSetting(PARTITIONS_HIDDEN_COLUMNS_KEY); diff --git a/src/containers/Tenant/Diagnostics/Partitions/utils/index.ts b/src/containers/Tenant/Diagnostics/Partitions/utils/index.ts index 07a3fbb32..11d75420a 100644 --- a/src/containers/Tenant/Diagnostics/Partitions/utils/index.ts +++ b/src/containers/Tenant/Diagnostics/Partitions/utils/index.ts @@ -1,21 +1,21 @@ import type {PreparedPartitionData} from '../../../../../store/reducers/partitions/types'; -import type {NodeHostsMap} from '../../../../../types/store/nodesList'; +import type {NodesMap} from '../../../../../types/store/nodesList'; import type {PreparedPartitionDataWithHosts} from './types'; export const addHostToPartitions = ( partitions: PreparedPartitionData[] = [], - nodeHosts?: NodeHostsMap, + nodeHosts?: NodesMap, ): PreparedPartitionDataWithHosts[] => { return partitions?.map((partition) => { const partitionHost = partition.partitionNodeId && nodeHosts - ? nodeHosts.get(partition.partitionNodeId) + ? nodeHosts.get(partition.partitionNodeId)?.Host : undefined; const connectionHost = partition.connectionNodeId && nodeHosts - ? nodeHosts.get(partition.connectionNodeId) + ? nodeHosts.get(partition.connectionNodeId)?.Host : undefined; return { diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts index a5daafafe..4ba6ac4b7 100644 --- a/src/store/reducers/cluster/cluster.ts +++ b/src/store/reducers/cluster/cluster.ts @@ -13,7 +13,7 @@ import {CLUSTER_DEFAULT_TITLE, DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/con import {isQueryErrorResponse} from '../../../utils/query'; import type {RootState} from '../../defaultStore'; import {api} from '../api'; -import {selectNodeHostsMap} from '../nodesList'; +import {selectNodesMap} from '../nodesList'; import type {ClusterGroupsStats, ClusterState} from './types'; import { @@ -181,7 +181,7 @@ export const selectClusterTitle = createSelector( export const selectClusterTabletsWithFqdn = createSelector( (state: RootState, clusterName?: string) => selectClusterInfo(state, clusterName), - (state: RootState) => selectNodeHostsMap(state), + (state: RootState) => selectNodesMap(state), (data, nodeHostsMap): (TTabletStateInfo & {fqdn?: string})[] => { const tablets = data?.clusterData?.SystemTablets; if (!tablets) { @@ -191,7 +191,8 @@ export const selectClusterTabletsWithFqdn = createSelector( return tablets; } return tablets.map((tablet) => { - const fqdn = tablet.NodeId === undefined ? undefined : nodeHostsMap.get(tablet.NodeId); + const fqdn = + tablet.NodeId === undefined ? undefined : nodeHostsMap.get(tablet.NodeId)?.Host; return {...tablet, fqdn}; }); }, diff --git a/src/store/reducers/nodesList.ts b/src/store/reducers/nodesList.ts index 153c64d43..79d6ccd2a 100644 --- a/src/store/reducers/nodesList.ts +++ b/src/store/reducers/nodesList.ts @@ -1,6 +1,6 @@ import {createSelector} from '@reduxjs/toolkit'; -import {prepareNodeHostsMap} from '../../utils/nodes'; +import {prepareNodesMap} from '../../utils/nodes'; import type {RootState} from '../defaultStore'; import {api} from './api'; @@ -23,7 +23,7 @@ export const nodesListApi = api.injectEndpoints({ const selectNodesList = nodesListApi.endpoints.getNodesList.select(undefined); -export const selectNodeHostsMap = createSelector( +export const selectNodesMap = createSelector( (state: RootState) => selectNodesList(state).data, - (data) => prepareNodeHostsMap(data), + (data) => prepareNodesMap(data), ); diff --git a/src/store/reducers/tablet.ts b/src/store/reducers/tablet.ts index 5ba14f781..2e8f099be 100644 --- a/src/store/reducers/tablet.ts +++ b/src/store/reducers/tablet.ts @@ -1,6 +1,6 @@ import type {TDomainKey} from '../../types/api/tablet'; import type {ITabletPreparedHistoryItem} from '../../types/store/tablet'; -import {prepareNodeHostsMap} from '../../utils/nodes'; +import {prepareNodesMap} from '../../utils/nodes'; import {api} from './api'; @@ -17,7 +17,7 @@ export const tabletApi = api.injectEndpoints({ window.api.viewer.getTabletHistory({id, database}, {signal}), window.api.viewer.getNodesList({signal}), ]); - const nodeHostsMap = prepareNodeHostsMap(nodesList); + const nodeHostsMap = prepareNodesMap(nodesList); const historyData = Object.keys(historyResponseData).reduce< ITabletPreparedHistoryItem[] @@ -31,7 +31,7 @@ export const tabletApi = api.injectEndpoints({ const fqdn = nodeHostsMap && nodeId - ? nodeHostsMap.get(Number(nodeId)) + ? nodeHostsMap.get(Number(nodeId))?.Host : undefined; if (State !== 'Dead') { diff --git a/src/store/reducers/tablets.ts b/src/store/reducers/tablets.ts index 8e1f97b99..a6df446e8 100644 --- a/src/store/reducers/tablets.ts +++ b/src/store/reducers/tablets.ts @@ -6,7 +6,7 @@ import type {TabletsApiRequestParams} from '../../types/store/tablets'; import type {RootState} from '../defaultStore'; import {api} from './api'; -import {selectNodeHostsMap} from './nodesList'; +import {selectNodesMap} from './nodesList'; export const tabletsApi = api.injectEndpoints({ endpoints: (build) => ({ @@ -42,7 +42,7 @@ const selectGetTabletsInfo = createSelector( export const selectTabletsWithFqdn = createSelector( (state: RootState, params: TabletsApiRequestParams) => selectGetTabletsInfo(state, params), - (state: RootState) => selectNodeHostsMap(state), + (state: RootState) => selectNodesMap(state), (data, nodeHostsMap): (TTabletStateInfo & {fqdn?: string})[] => { if (!data?.TabletStateInfo) { return []; @@ -51,7 +51,8 @@ export const selectTabletsWithFqdn = createSelector( return data.TabletStateInfo; } return data.TabletStateInfo.map((tablet) => { - const fqdn = tablet.NodeId === undefined ? undefined : nodeHostsMap.get(tablet.NodeId); + const fqdn = + tablet.NodeId === undefined ? undefined : nodeHostsMap.get(tablet.NodeId)?.Host; return {...tablet, fqdn}; }); }, diff --git a/src/types/store/nodesList.ts b/src/types/store/nodesList.ts index 91786397e..85e8f50c7 100644 --- a/src/types/store/nodesList.ts +++ b/src/types/store/nodesList.ts @@ -1 +1,7 @@ -export type NodeHostsMap = Map; +export type NodesMap = Map< + number, + { + Host?: string; + DC?: string; + } +>; diff --git a/src/utils/nodes.ts b/src/utils/nodes.ts index db827bae4..48c34d568 100644 --- a/src/utils/nodes.ts +++ b/src/utils/nodes.ts @@ -5,7 +5,7 @@ import type {ProblemFilterValue} from '../store/reducers/settings/types'; import {EFlag} from '../types/api/enums'; import type {TSystemStateInfo} from '../types/api/nodes'; import type {TNodeInfo} from '../types/api/nodesList'; -import type {NodeHostsMap} from '../types/store/nodesList'; +import type {NodesMap} from '../types/store/nodesList'; import {HOUR_IN_SECONDS} from './constants'; @@ -31,10 +31,13 @@ export const isUnavailableNode = < node: T, ) => !node.SystemState || node.SystemState === EFlag.Grey; -export const prepareNodeHostsMap = (nodesList?: TNodeInfo[]) => { - return nodesList?.reduce((nodeHosts, node) => { - if (node.Id && node.Host) { - nodeHosts.set(Number(node.Id), node.Host); +export const prepareNodesMap = (nodesList?: TNodeInfo[]) => { + return nodesList?.reduce((nodeHosts, node) => { + if (valueIsDefined(node.Id)) { + nodeHosts.set(node.Id, { + Host: node.Host, + DC: node.PhysicalLocation?.DataCenterId, + }); } return nodeHosts; }, new Map());