diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss index c9b916b643..e8ceb76438 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss @@ -10,6 +10,9 @@ --progress-bar-full-height: var(--g-text-body-3-line-height); --progress-bar-compact-height: 12px; + --stripe-width: 4px; + --stripe-step: 8px; + position: relative; z-index: 0; @@ -45,6 +48,20 @@ background-color: unset; } + &_striped { + overflow: hidden; + // Inset shadow = border overlay without shrinking the striped fill + border: none; + background-image: repeating-linear-gradient( + 135deg, + transparent 0, + transparent var(--stripe-width), + var(--entity-state-fill-color) var(--stripe-width), + var(--entity-state-fill-color) var(--stripe-step) + ); + box-shadow: 0 0 0 $border-width var(--entity-state-shadow-color) inset; + } + &__fill-bar { position: absolute; top: 0; @@ -76,10 +93,21 @@ position: relative; z-index: 2; + margin-right: var(--g-spacing-1); + font-size: var(--g-text-body-1-font-size); // bar height minus borders line-height: calc(var(--progress-bar-full-height) - #{$border-width * 2}); color: inherit; } + + &__icon { + position: relative; + z-index: 2; + + margin-left: var(--g-spacing-1); + + color: var(--entity-state-border-color); + } } diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx index e5e4252734..ac93c6ab82 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx @@ -1,8 +1,11 @@ import React from 'react'; +import {Flex, Icon} from '@gravity-ui/uikit'; + import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {cn} from '../../utils/cn'; -import {getSeverityColor} from '../../utils/disks/helpers'; +import {DONOR_COLOR} from '../../utils/disks/constants'; +import {getSeverityColor, getVDiskStatusIcon} from '../../utils/disks/helpers'; import {useSetting} from '../../utils/hooks'; import './DiskStateProgressBar.scss'; @@ -16,8 +19,11 @@ interface DiskStateProgressBarProps { faded?: boolean; inactive?: boolean; empty?: boolean; + striped?: boolean; content?: React.ReactNode; className?: string; + isDonor?: boolean; + withIcon?: boolean; } export function DiskStateProgressBar({ @@ -28,15 +34,29 @@ export function DiskStateProgressBar({ inactive, empty, content, + striped, className, + isDonor, + withIcon, }: DiskStateProgressBarProps) { const [inverted] = useSetting(SETTING_KEYS.INVERTED_DISKS); - const mods: Record = {inverted, compact, faded, empty, inactive}; + const mods: Record = { + inverted, + compact, + faded, + empty, + inactive, + striped, + }; - const color = severity !== undefined && getSeverityColor(severity); - if (color) { - mods[color.toLocaleLowerCase()] = true; + if (isDonor) { + mods[DONOR_COLOR.toLocaleLowerCase()] = true; + } else { + const color = severity !== undefined && getSeverityColor(severity); + if (color) { + mods[color.toLocaleLowerCase()] = true; + } } const renderAllocatedPercent = () => { @@ -69,8 +89,23 @@ export function DiskStateProgressBar({ return null; }; + let iconElement: React.ReactNode = null; + + if (withIcon) { + const icon = getVDiskStatusIcon(severity, isDonor); + + if (icon) { + iconElement = ; + } + } + + const hasIcon = Boolean(iconElement); + const justifyContent = hasIcon ? 'space-between' : 'flex-end'; + return ( -
+ {iconElement} {renderAllocatedPercent()} {renderContent()} -
+ ); } diff --git a/src/components/PDiskPopup/PDiskPopup.tsx b/src/components/PDiskPopup/PDiskPopup.tsx index 9bf2493369..39cdfe3a23 100644 --- a/src/components/PDiskPopup/PDiskPopup.tsx +++ b/src/components/PDiskPopup/PDiskPopup.tsx @@ -1,103 +1,129 @@ import React from 'react'; -import {Flex} from '@gravity-ui/uikit'; +import {Flex, Label} from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; -import {valueIsDefined} from '../../utils'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import {createPDiskDeveloperUILink} from '../../utils/developerUI/developerUI'; +import {getStateSeverity} from '../../utils/disks/calculatePDiskSeverity'; +import {NUMERIC_SEVERITY_TO_LABEL_VIEW} from '../../utils/disks/constants'; import type {PreparedPDisk} from '../../utils/disks/types'; import {useTypedSelector} from '../../utils/hooks'; import {useDatabaseFromQuery} from '../../utils/hooks/useDatabaseFromQuery'; import {useIsUserAllowedToMakeChanges} from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {bytesToGB, isNumeric} from '../../utils/utils'; -import {InfoViewer} from '../InfoViewer'; -import type {InfoViewerItem} from '../InfoViewer'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; import {pDiskInfoKeyset} from '../PDiskInfo/i18n'; import {PDiskPageLink} from '../PDiskPageLink/PDiskPageLink'; +import {StatusIcon} from '../StatusIcon/StatusIcon'; +import type { + YDBDefinitionListHeaderLabel, + YDBDefinitionListItem, +} from '../YDBDefinitionList/YDBDefinitionList'; +import {YDBDefinitionList} from '../YDBDefinitionList/YDBDefinitionList'; + +import {pDiskPopupKeyset} from './i18n'; const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow]; -export const preparePDiskData = ( - data: PreparedPDisk, - nodeData?: {Host?: string; DC?: string}, - withDeveloperUILink?: boolean, -) => { - const { - AvailableSize, - TotalSize, - State, - PDiskId, - NodeId, - StringifiedId, - Path, - Realtime, - Type, - Device, - } = data; - - const pdiskData: InfoViewerItem[] = [ - { - label: 'PDisk', - value: StringifiedId ?? EMPTY_DATA_PLACEHOLDER, - }, - {label: 'State', value: State || 'not available'}, - {label: 'Type', value: Type || 'unknown'}, +export const preparePDiskData = (data: PreparedPDisk, nodeData?: {Host?: string; DC?: string}) => { + const {AvailableSize, TotalSize, NodeId, Path, Realtime, Type, Device} = data; + + const pdiskData: YDBDefinitionListItem[] = [ + {name: pDiskPopupKeyset('label_type'), content: Type || pDiskPopupKeyset('value_unknown')}, ]; if (NodeId) { - pdiskData.push({label: 'Node Id', value: NodeId}); + pdiskData.push({name: pDiskPopupKeyset('label_node-id'), content: NodeId}); } if (nodeData?.Host) { - pdiskData.push({label: 'Host', value: nodeData.Host}); + pdiskData.push({name: pDiskPopupKeyset('label_host'), content: nodeData.Host}); } + if (nodeData?.DC) { - pdiskData.push({label: 'DC', value: nodeData.DC}); + pdiskData.push({name: pDiskPopupKeyset('label_dc'), content: }); } if (Path) { - pdiskData.push({label: 'Path', value: Path}); + pdiskData.push({name: pDiskPopupKeyset('label_path'), content: Path}); } - if (isNumeric(TotalSize)) { + if (isNumeric(TotalSize) && isNumeric(AvailableSize)) { pdiskData.push({ - label: 'Available', - value: `${bytesToGB(AvailableSize)} of ${bytesToGB(TotalSize)}`, + name: pDiskPopupKeyset('label_available'), + content: `${bytesToGB(AvailableSize)} ${pDiskPopupKeyset('value_of')} ${bytesToGB(TotalSize)}`, }); } if (Realtime && errorColors.includes(Realtime)) { - pdiskData.push({label: 'Realtime', value: Realtime}); + pdiskData.push({ + name: pDiskPopupKeyset('label_realtime'), + content: , + }); } if (Device && errorColors.includes(Device)) { - pdiskData.push({label: 'Device', value: Device}); + pdiskData.push({ + name: pDiskPopupKeyset('label_device'), + content: , + }); } - if (withDeveloperUILink && valueIsDefined(NodeId) && valueIsDefined(PDiskId)) { - const pDiskInternalViewerPath = createPDiskDeveloperUILink({ - nodeId: NodeId, - pDiskId: PDiskId, + return pdiskData; +}; + +export const preparePDiskHeaderLabels = (data: PreparedPDisk): YDBDefinitionListHeaderLabel[] => { + const labels: YDBDefinitionListHeaderLabel[] = []; + const {State} = data; + + if (!State) { + labels.push({ + id: 'state', + value: pDiskPopupKeyset('context_not-available'), }); - pdiskData.push({ - label: 'Links', - value: ( - - - - - ), + return labels; + } + + if (State) { + const severity = getStateSeverity(State); + const {theme, icon} = NUMERIC_SEVERITY_TO_LABEL_VIEW[severity]; + + labels.push({ + id: 'state', + value: State, + theme: theme, + icon: icon, }); } - return pdiskData; + return labels; +}; + +export const buildPDiskFooter = ( + data: PreparedPDisk, + withDeveloperUILink?: boolean, +): React.ReactNode | null => { + const {NodeId, PDiskId} = data; + + if (!withDeveloperUILink || isNil(NodeId) || isNil(PDiskId)) { + return null; + } + + const pDiskInternalViewerPath = createPDiskDeveloperUILink({ + nodeId: NodeId, + pDiskId: PDiskId, + }); + + return ( + + + + + ); }; interface PDiskPopupProps { @@ -108,11 +134,31 @@ export const PDiskPopup = ({data}: PDiskPopupProps) => { const database = useDatabaseFromQuery(); const isUserAllowedToMakeChanges = useIsUserAllowedToMakeChanges(); const nodesMap = useTypedSelector((state) => selectNodesMap(state, database)); - const nodeData = valueIsDefined(data.NodeId) ? nodesMap?.get(data.NodeId) : undefined; - const info = React.useMemo( - () => preparePDiskData(data, nodeData, isUserAllowedToMakeChanges), - [data, nodeData, isUserAllowedToMakeChanges], + const nodeData = isNil(data.NodeId) ? undefined : nodesMap?.get(data.NodeId); + + const info = React.useMemo(() => preparePDiskData(data, nodeData), [data, nodeData]); + + const headerLabels = React.useMemo( + () => preparePDiskHeaderLabels(data), + [data], + ); + + const footer = React.useMemo( + () => buildPDiskFooter(data, isUserAllowedToMakeChanges), + [data, isUserAllowedToMakeChanges], ); - return ; + const pdiskId = data.StringifiedId; + + return ( + + ); }; diff --git a/src/components/PDiskPopup/i18n/en.json b/src/components/PDiskPopup/i18n/en.json new file mode 100644 index 0000000000..b026329f08 --- /dev/null +++ b/src/components/PDiskPopup/i18n/en.json @@ -0,0 +1,13 @@ +{ + "context_not-available": "Not available", + "label_type": "Type", + "label_node-id": "Node Id", + "label_host": "Host", + "label_dc": "DC", + "label_path": "Path", + "label_available": "Available", + "label_realtime": "Realtime", + "label_device": "Device", + "value_unknown": "unknown", + "value_of": "of" +} diff --git a/src/components/PDiskPopup/i18n/index.ts b/src/components/PDiskPopup/i18n/index.ts new file mode 100644 index 0000000000..d6b98fd5c5 --- /dev/null +++ b/src/components/PDiskPopup/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-pDisk-popup'; + +export const pDiskPopupKeyset = registerKeysets(COMPONENT, {en}); diff --git a/src/components/StatusIcon/StatusIcon.scss b/src/components/StatusIcon/StatusIcon.scss index 94c2f37f2b..d6fc115793 100644 --- a/src/components/StatusIcon/StatusIcon.scss +++ b/src/components/StatusIcon/StatusIcon.scss @@ -16,7 +16,7 @@ background-color: var(--ydb-color-status-grey); } &_state_orange { - background-color: var(--ydb-color-status-orange); + background-color: var(--ydb-color-status-red); } } @@ -34,7 +34,7 @@ color: var(--ydb-color-status-yellow); } &_state_orange { - color: var(--ydb-color-status-orange); + color: var(--ydb-color-status-red); } &_state_red { color: var(--ydb-color-status-red); diff --git a/src/components/StatusIcon/StatusIcon.tsx b/src/components/StatusIcon/StatusIcon.tsx index 734ea5b629..dcd2150450 100644 --- a/src/components/StatusIcon/StatusIcon.tsx +++ b/src/components/StatusIcon/StatusIcon.tsx @@ -3,6 +3,7 @@ import { CircleExclamationFill, CircleInfoFill, CircleQuestionFill, + CircleXmarkFill, TriangleExclamationFill, } from '@gravity-ui/icons'; import {Icon} from '@gravity-ui/uikit'; @@ -18,9 +19,9 @@ const icons = { [EFlag.Grey]: CircleQuestionFill, [EFlag.Green]: CircleCheckFill, [EFlag.Blue]: CircleInfoFill, - [EFlag.Yellow]: CircleExclamationFill, - [EFlag.Orange]: TriangleExclamationFill, - [EFlag.Red]: CircleExclamationFill, + [EFlag.Yellow]: TriangleExclamationFill, + [EFlag.Orange]: CircleExclamationFill, + [EFlag.Red]: CircleXmarkFill, }; export type StatusIconMode = 'color' | 'icons'; diff --git a/src/components/StatusIconNew/StatusIcon.tsx b/src/components/StatusIconNew/StatusIcon.tsx index 7808c7edc1..4ead7021f0 100644 --- a/src/components/StatusIconNew/StatusIcon.tsx +++ b/src/components/StatusIconNew/StatusIcon.tsx @@ -1,21 +1,21 @@ import { - CircleCheck, - CircleExclamation, + CircleCheckFill, + CircleExclamationFill, CircleInfo, PlugConnection, - TriangleExclamation, + TriangleExclamationFill, } from '@gravity-ui/icons'; -import type {IconProps} from '@gravity-ui/uikit'; +import type {IconData, IconProps} from '@gravity-ui/uikit'; import {Icon} from '@gravity-ui/uikit'; import {EFlag} from '../../types/api/enums'; -const EFlagToIcon: Record) => React.JSX.Element> = { +const EFlagToIcon: Record = { [EFlag.Blue]: CircleInfo, - [EFlag.Yellow]: CircleExclamation, - [EFlag.Orange]: TriangleExclamation, - [EFlag.Red]: CircleExclamation, - [EFlag.Green]: CircleCheck, + [EFlag.Yellow]: CircleExclamationFill, + [EFlag.Orange]: TriangleExclamationFill, + [EFlag.Red]: CircleExclamationFill, + [EFlag.Green]: CircleCheckFill, [EFlag.Grey]: PlugConnection, }; @@ -27,5 +27,6 @@ export function StatusIcon({status, ...props}: StatusIconProps) { if (!status) { return null; } + return ; } diff --git a/src/components/VDisk/VDisk.tsx b/src/components/VDisk/VDisk.tsx index 0459bf816b..76eb71ce2e 100644 --- a/src/components/VDisk/VDisk.tsx +++ b/src/components/VDisk/VDisk.tsx @@ -1,5 +1,6 @@ import {useVDiskPagePath} from '../../routes'; import {cn} from '../../utils/cn'; +import {DISK_COLOR_STATE_TO_NUMERIC_SEVERITY} from '../../utils/disks/constants'; import type {PreparedVDisk} from '../../utils/disks/types'; import {DiskStateProgressBar} from '../DiskStateProgressBar/DiskStateProgressBar'; import {HoverPopup} from '../HoverPopup/HoverPopup'; @@ -20,6 +21,7 @@ export interface VDiskProps { progressBarClassName?: string; delayOpen?: number; delayClose?: number; + withIcon?: boolean; } export const VDisk = ({ @@ -32,10 +34,15 @@ export const VDisk = ({ progressBarClassName, delayClose, delayOpen, + withIcon, }: VDiskProps) => { const getVDiskLink = useVDiskPagePath(); const vDiskPath = getVDiskLink({nodeId: data.NodeId, vDiskId: data.StringifiedId}); + const severity = data.Severity; + const isReplicatingColor = severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue; + const isHealthyDonor = data.DonorMode && isReplicatingColor; + return ( diff --git a/src/components/VDisk/VDiskWithDonorsStack.tsx b/src/components/VDisk/VDiskWithDonorsStack.tsx index f1d971f11e..e1411310d8 100644 --- a/src/components/VDisk/VDiskWithDonorsStack.tsx +++ b/src/components/VDisk/VDiskWithDonorsStack.tsx @@ -16,6 +16,7 @@ export function VDiskWithDonorsStack({ data, className, stackClassName, + withIcon, ...restProps }: VDiskWithDonorsStackProps) { const {Donors: donors, ...restData} = data || {}; @@ -23,7 +24,7 @@ export function VDiskWithDonorsStack({ const content = donors && donors.length > 0 ? ( - + {donors.map((donor) => { const isFullData = isFullVDiskData(donor); @@ -32,13 +33,14 @@ export function VDiskWithDonorsStack({ ); })} ) : ( - + ); return
{content}
; diff --git a/src/components/VDiskPopup/VDiskPopup.scss b/src/components/VDiskPopup/VDiskPopup.scss index c98fcc9fcc..0efeb57e06 100644 --- a/src/components/VDiskPopup/VDiskPopup.scss +++ b/src/components/VDiskPopup/VDiskPopup.scss @@ -6,7 +6,7 @@ border-top: 1px solid var(--g-color-line-generic); } - &__donor-label { - margin-bottom: 8px; + &__custom-divider { + margin: var(--g-spacing-3) 0; } } diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 7e61bb28a4..929face6cc 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -1,16 +1,23 @@ import React from 'react'; -import {Flex, Label} from '@gravity-ui/uikit'; +import {Divider, Flex} from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; import {useVDiskPagePath} from '../../routes'; import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; import {EVDiskState} from '../../types/api/vdisk'; -import {valueIsDefined} from '../../utils'; import {cn} from '../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import {formatUptimeInSeconds} from '../../utils/dataFormatters/dataFormatters'; import {createVDiskDeveloperUILink} from '../../utils/developerUI/developerUI'; +import {getStateSeverity} from '../../utils/disks/calculateVDiskSeverity'; +import { + DISK_COLOR_STATE_TO_NUMERIC_SEVERITY, + NOT_AVAILABLE_SEVERITY, + NUMERIC_SEVERITY_TO_LABEL_VIEW, + VDISK_LABEL_CONFIG, +} from '../../utils/disks/constants'; import {isFullVDiskData} from '../../utils/disks/helpers'; import type {PreparedVDisk, UnavailableDonor} from '../../utils/disks/types'; import {useTypedSelector} from '../../utils/hooks'; @@ -20,12 +27,20 @@ import { useIsViewerUser, } from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {bytesToGB, bytesToSpeed} from '../../utils/utils'; -import type {InfoViewerItem} from '../InfoViewer'; -import {InfoViewer} from '../InfoViewer'; import {InternalLink} from '../InternalLink'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; -import {preparePDiskData} from '../PDiskPopup/PDiskPopup'; +import { + buildPDiskFooter, + preparePDiskData, + preparePDiskHeaderLabels, +} from '../PDiskPopup/PDiskPopup'; +import {StatusIcon} from '../StatusIcon/StatusIcon'; import {vDiskInfoKeyset} from '../VDiskInfo/i18n'; +import type { + YDBDefinitionListHeaderLabel, + YDBDefinitionListItem, +} from '../YDBDefinitionList/YDBDefinitionList'; +import {YDBDefinitionList} from '../YDBDefinitionList/YDBDefinitionList'; import {vDiskPopupKeyset} from './i18n'; @@ -33,58 +48,83 @@ import './VDiskPopup.scss'; const b = cn('vdisk-storage-popup'); -const prepareUnavailableVDiskData = (data: UnavailableDonor, withDeveloperUILink?: boolean) => { +const prepareUnavailableVDiskData = (data: UnavailableDonor) => { const {NodeId, PDiskId, VSlotId, StoragePoolName} = data; - const vdiskData: InfoViewerItem[] = [ - {label: vDiskPopupKeyset('label_state'), value: vDiskPopupKeyset('context_not-available')}, - ]; + const vdiskData: YDBDefinitionListItem[] = []; if (StoragePoolName) { - vdiskData.push({label: vDiskPopupKeyset('label_storage-pool'), value: StoragePoolName}); + vdiskData.push({name: vDiskPopupKeyset('label_storage-pool'), content: StoragePoolName}); } vdiskData.push( - {label: vDiskPopupKeyset('label_node-id'), value: NodeId ?? EMPTY_DATA_PLACEHOLDER}, - {label: vDiskPopupKeyset('label_pdisk-id'), value: PDiskId ?? EMPTY_DATA_PLACEHOLDER}, - {label: vDiskPopupKeyset('label_vslot-id'), value: VSlotId ?? EMPTY_DATA_PLACEHOLDER}, + {name: vDiskPopupKeyset('label_node-id'), content: NodeId ?? EMPTY_DATA_PLACEHOLDER}, + {name: vDiskPopupKeyset('label_pdisk-id'), content: PDiskId ?? EMPTY_DATA_PLACEHOLDER}, + {name: vDiskPopupKeyset('label_vslot-id'), content: VSlotId ?? EMPTY_DATA_PLACEHOLDER}, ); - if ( - withDeveloperUILink && - valueIsDefined(NodeId) && - valueIsDefined(PDiskId) && - valueIsDefined(VSlotId) - ) { - const vDiskInternalViewerPath = createVDiskDeveloperUILink({ - nodeId: NodeId, - pDiskId: PDiskId, - vDiskSlotId: VSlotId, - }); + return vdiskData; +}; - vdiskData.push({ - label: vDiskPopupKeyset('label_links'), - value: , - }); +const buildUnavailableVDiskFooter = ( + data: UnavailableDonor, + withDeveloperUILink?: boolean, +): React.ReactNode | null => { + const {NodeId, PDiskId, VSlotId} = data; + + if (!withDeveloperUILink || isNil(NodeId) || isNil(PDiskId) || isNil(VSlotId)) { + return null; } - return vdiskData; + const vDiskInternalViewerPath = createVDiskDeveloperUILink({ + nodeId: NodeId, + pDiskId: PDiskId, + vDiskSlotId: VSlotId, + }); + + return ( +
+ +
+ ); +}; + +interface VDiskLinkProps { + nodeId?: string | number; + stringifiedId?: string; + getVDiskLinkFn?: (data: { + nodeId: string | number; + vDiskId: string | undefined; + }) => string | undefined; +} + +const VDiskLink = ({nodeId, stringifiedId, getVDiskLinkFn}: VDiskLinkProps) => { + if (isNil(stringifiedId)) { + return {EMPTY_DATA_PLACEHOLDER}; + } + + if (isNil(nodeId)) { + return {stringifiedId}; + } + + const path = getVDiskLinkFn?.({nodeId, vDiskId: stringifiedId}); + + return ( + + {vDiskPopupKeyset('label_vdisk')} {stringifiedId} + + ); }; // eslint-disable-next-line complexity const prepareVDiskData = ( data: PreparedVDisk, - withDeveloperUILink: boolean | undefined, getVDiskLinkFn?: (data: { nodeId: string | number; vDiskId: string | undefined; }) => string | undefined, ) => { const { - NodeId, - PDiskId, - VDiskSlotId, - StringifiedId, VDiskState, SatisfactionRank, DiskSpace, @@ -97,143 +137,245 @@ const prepareVDiskData = ( ReadThroughput, WriteThroughput, StoragePoolName, + Donors, + DonorMode, + Recipient, + Severity, } = data; - const vdiskData: InfoViewerItem[] = [ - {label: vDiskPopupKeyset('label_vdisk'), value: StringifiedId}, - { - label: vDiskPopupKeyset('label_state'), - value: VDiskState ?? vDiskPopupKeyset('context_not-available'), - }, - ]; + const vdiskData: YDBDefinitionListItem[] = []; if (StoragePoolName) { - vdiskData.push({label: vDiskPopupKeyset('label_storage-pool'), value: StoragePoolName}); + vdiskData.push({name: vDiskPopupKeyset('label_storage-pool'), content: StoragePoolName}); + } + + // it is a healthy replication and it has some donors + if (Donors?.length && Severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue) { + vdiskData.push({ + name: vDiskPopupKeyset('label_donor'), + content: ( + + {Donors.map((donor) => ( + + ))} + + ), + }); + } + + if (DonorMode && Recipient) { + vdiskData.push({ + name: vDiskPopupKeyset('label_recipient'), + content: ( + + ), + }); } if (SatisfactionRank && SatisfactionRank.FreshRank?.Flag !== EFlag.Green) { vdiskData.push({ - label: vDiskPopupKeyset('label_fresh'), - value: SatisfactionRank.FreshRank?.Flag, + name: vDiskPopupKeyset('label_fresh'), + content: SatisfactionRank.FreshRank?.Flag, }); } if (SatisfactionRank && SatisfactionRank.LevelRank?.Flag !== EFlag.Green) { vdiskData.push({ - label: vDiskPopupKeyset('label_level'), - value: SatisfactionRank.LevelRank?.Flag, + name: vDiskPopupKeyset('label_level'), + content: SatisfactionRank.LevelRank?.Flag, }); } if (SatisfactionRank && SatisfactionRank.FreshRank?.RankPercent) { vdiskData.push({ - label: vDiskPopupKeyset('label_fresh'), - value: SatisfactionRank.FreshRank.RankPercent, + name: vDiskPopupKeyset('label_fresh'), + content: SatisfactionRank.FreshRank.RankPercent, }); } if (SatisfactionRank && SatisfactionRank.LevelRank?.RankPercent) { vdiskData.push({ - label: vDiskPopupKeyset('label_level'), - value: SatisfactionRank.LevelRank.RankPercent, + name: vDiskPopupKeyset('label_level'), + content: SatisfactionRank.LevelRank.RankPercent, }); } - if (DiskSpace && DiskSpace !== EFlag.Green) { - vdiskData.push({label: vDiskPopupKeyset('label_space'), value: DiskSpace}); + if (DiskSpace) { + vdiskData.push({ + name: vDiskPopupKeyset('label_space'), + content: , + }); } - if (FrontQueues && FrontQueues !== EFlag.Green) { - vdiskData.push({label: vDiskPopupKeyset('label_front-queues'), value: FrontQueues}); + if (FrontQueues) { + vdiskData.push({ + name: vDiskPopupKeyset('label_front-queues'), + content: , + }); } if (Replicated === false && VDiskState === EVDiskState.OK) { - vdiskData.push({label: vDiskPopupKeyset('label_replicated'), value: 'NO'}); + vdiskData.push({name: vDiskPopupKeyset('label_replicated'), content: 'NO'}); // Only show replication progress and time remaining when disk is not replicated and state is OK - if (valueIsDefined(ReplicationProgress)) { + if (!isNil(ReplicationProgress)) { const progressPercent = Math.round(ReplicationProgress * 100); vdiskData.push({ - label: vDiskPopupKeyset('label_progress'), - value: `${progressPercent}%`, + name: vDiskPopupKeyset('label_progress'), + content: `${progressPercent}%`, }); } - if (valueIsDefined(ReplicationSecondsRemaining)) { + if (!isNil(ReplicationSecondsRemaining)) { const timeRemaining = formatUptimeInSeconds(ReplicationSecondsRemaining); if (timeRemaining) { vdiskData.push({ - label: vDiskPopupKeyset('label_remaining'), - value: timeRemaining, + name: vDiskPopupKeyset('label_remaining'), + content: timeRemaining, }); } } } if (UnsyncedVDisks) { - vdiskData.push({label: vDiskPopupKeyset('label_unsync-vdisks'), value: UnsyncedVDisks}); + vdiskData.push({name: vDiskPopupKeyset('label_unsync-vdisks'), content: UnsyncedVDisks}); } if (Number(AllocatedSize)) { vdiskData.push({ - label: vDiskPopupKeyset('label_allocated'), - value: bytesToGB(AllocatedSize), + name: vDiskPopupKeyset('label_allocated'), + content: bytesToGB(AllocatedSize), }); } if (Number(ReadThroughput)) { vdiskData.push({ - label: vDiskPopupKeyset('label_read'), - value: bytesToSpeed(ReadThroughput), + name: vDiskPopupKeyset('label_read'), + content: bytesToSpeed(ReadThroughput), }); } if (Number(WriteThroughput)) { vdiskData.push({ - label: vDiskPopupKeyset('label_write'), - value: bytesToSpeed(WriteThroughput), + name: vDiskPopupKeyset('label_write'), + content: bytesToSpeed(WriteThroughput), }); } - if ( - withDeveloperUILink && - valueIsDefined(NodeId) && - valueIsDefined(PDiskId) && - (valueIsDefined(VDiskSlotId) || valueIsDefined(StringifiedId)) - ) { - const vDiskInternalViewerPath = valueIsDefined(VDiskSlotId) - ? createVDiskDeveloperUILink({ - nodeId: NodeId, - pDiskId: PDiskId, - vDiskSlotId: VDiskSlotId, - }) - : undefined; - - const vDiskPagePath = getVDiskLinkFn?.({nodeId: NodeId, vDiskId: StringifiedId}); - if (vDiskPagePath) { - vdiskData.push({ - label: vDiskPopupKeyset('label_links'), - value: ( - - - {vDiskInternalViewerPath ? ( - - ) : null} - - ), + return vdiskData; +}; + +const buildVDiskFooter = ( + data: PreparedVDisk, + withDeveloperUILink?: boolean, + getVDiskLinkFn?: (data: { + nodeId: string | number; + vDiskId: string | undefined; + }) => string | undefined, +): React.ReactNode | null => { + if (!withDeveloperUILink) { + return null; + } + + const {NodeId, PDiskId, VDiskSlotId, StringifiedId} = data; + + if (isNil(NodeId) || isNil(PDiskId) || (isNil(VDiskSlotId) && isNil(StringifiedId))) { + return null; + } + + const vDiskInternalViewerPath = isNil(VDiskSlotId) + ? undefined + : createVDiskDeveloperUILink({ + nodeId: NodeId, + pDiskId: PDiskId, + vDiskSlotId: VDiskSlotId, + }); + + const vDiskPagePath = getVDiskLinkFn?.({ + nodeId: NodeId, + vDiskId: StringifiedId, + }); + + if (!vDiskPagePath) { + return null; + } + + return ( + + {vDiskPagePath && ( + + )} + {vDiskInternalViewerPath && ( + + )} + + ); +}; + +const prepareHeaderLabels = (data: PreparedVDisk): YDBDefinitionListHeaderLabel[] => { + const labels: YDBDefinitionListHeaderLabel[] = []; + + const {VDiskState, DonorMode, Severity} = data; + + const isReplicatingColor = Severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue; + + if (DonorMode) { + const donorConfig = VDISK_LABEL_CONFIG.donor; + + labels.push({ + id: 'donor', + value: vDiskPopupKeyset('label_donor'), + theme: donorConfig.theme, + icon: donorConfig.icon, + }); + } + + if (isReplicatingColor) { + if (!DonorMode) { + const replicaConfig = VDISK_LABEL_CONFIG.replica; + + labels.push({ + id: 'replication', + value: vDiskPopupKeyset('label_replication'), + theme: replicaConfig.theme, + icon: replicaConfig.icon, }); } + + return labels; } - return vdiskData; + const severity = VDiskState ? getStateSeverity(VDiskState) : NOT_AVAILABLE_SEVERITY; + + const {theme: stateTheme, icon: stateIcon} = NUMERIC_SEVERITY_TO_LABEL_VIEW[severity]; + + const value = VDiskState ?? vDiskPopupKeyset('label_no-data'); + + labels.push({ + id: 'state', + value, + theme: stateTheme, + icon: stateIcon, + }); + + return labels; }; interface VDiskPopupProps { @@ -250,46 +392,71 @@ export const VDiskPopup = ({data}: VDiskPopupProps) => { const database = useDatabaseFromQuery(); const vdiskInfo = React.useMemo( + () => + isFullData ? prepareVDiskData(data, getVDiskLink) : prepareUnavailableVDiskData(data), + [data, isFullData, getVDiskLink], + ); + + const vdiskHeaderLabels: YDBDefinitionListHeaderLabel[] = React.useMemo( + () => (isFullData ? prepareHeaderLabels(data) : []), + [data, isFullData], + ); + + const vdiskFooter = React.useMemo( () => isFullData - ? prepareVDiskData(data, isUserAllowedToMakeChanges, getVDiskLink) - : prepareUnavailableVDiskData(data, isUserAllowedToMakeChanges), + ? buildVDiskFooter(data, isUserAllowedToMakeChanges, getVDiskLink) + : buildUnavailableVDiskFooter(data, isUserAllowedToMakeChanges), [data, isFullData, isUserAllowedToMakeChanges, getVDiskLink], ); const nodesMap = useTypedSelector((state) => selectNodesMap(state, database)); - const nodeData = valueIsDefined(data.NodeId) ? nodesMap?.get(data.NodeId) : undefined; + const nodeData = isNil(data.NodeId) ? undefined : nodesMap?.get(data.NodeId); const pdiskInfo = React.useMemo( + () => isFullData && data.PDisk && preparePDiskData(data.PDisk, nodeData), + [data, nodeData, isFullData], + ); + const pdiskHeaderLabels = React.useMemo( + () => (isFullData && data.PDisk ? preparePDiskHeaderLabels(data.PDisk) : []), + [data, isFullData], + ); + + const pdiskFooter = React.useMemo( () => - isFullData && - data.PDisk && - preparePDiskData(data.PDisk, nodeData, isUserAllowedToMakeChanges), - [data, nodeData, isFullData, isUserAllowedToMakeChanges], + isFullData && data.PDisk + ? buildPDiskFooter(data.PDisk, isUserAllowedToMakeChanges) + : null, + [data, isFullData, isUserAllowedToMakeChanges], ); - const donorsInfo: InfoViewerItem[] = []; - if ('Donors' in data && data.Donors) { - const donors = data.Donors; - for (const donor of donors) { - donorsInfo.push({ - label: vDiskPopupKeyset('label_vdisk'), - value: ( - - {donor.StringifiedId} - - ), - }); - } - } + const vdiskId = isFullData ? data.StringifiedId : undefined; + const pdiskId = isFullData ? data.PDisk?.StringifiedId : undefined; return (
- {data.DonorMode && } - - {pdiskInfo && isViewerUser && } - {donorsInfo.length > 0 && } + + {pdiskInfo && isViewerUser && ( + + + + + )}
); }; diff --git a/src/components/VDiskPopup/i18n/en.json b/src/components/VDiskPopup/i18n/en.json index 4d82fa9a17..1d4a165832 100644 --- a/src/components/VDiskPopup/i18n/en.json +++ b/src/components/VDiskPopup/i18n/en.json @@ -1,7 +1,8 @@ { "context_not-available": "not available", - "label_state": "State", + "label_no-data": "No data", "label_storage-pool": "StoragePool", + "label_donor": "Donor", "label_node-id": "NodeId", "label_pdisk-id": "PDiskId", "label_vslot-id": "VSlotId", @@ -17,5 +18,7 @@ "label_unsync-vdisks": "UnsyncVDisks", "label_allocated": "Allocated", "label_read": "Read", - "label_write": "Write" + "label_write": "Write", + "label_replication": "Replication", + "label_recipient": "Recipient" } diff --git a/src/components/YDBDefinitionList/YDBDefinitionList.scss b/src/components/YDBDefinitionList/YDBDefinitionList.scss index de6da797cb..b54f57ba88 100644 --- a/src/components/YDBDefinitionList/YDBDefinitionList.scss +++ b/src/components/YDBDefinitionList/YDBDefinitionList.scss @@ -1,13 +1,38 @@ @use '../../styles/mixins.scss'; .ydb-definition-list { + $block: &; @include mixins.flex-container(); &__title { @include mixins.info-viewer-title(); } + &__title-suffix { + color: var(--g-color-text-secondary); + } + &__properties-list { max-width: calc(100% - 40px); } + + &__footer { + margin-top: var(--g-spacing-3); + } + + &_compact { + #{$block}__title { + margin: 0; + + font-weight: 500; + } + + #{$block}__header { + margin-bottom: var(--g-spacing-3); + } + + #{$block}__properties-list { + --g-definition-list-item-gap: var(--g-spacing-2); + } + } } diff --git a/src/components/YDBDefinitionList/YDBDefinitionList.tsx b/src/components/YDBDefinitionList/YDBDefinitionList.tsx index 060d8c1418..b404886443 100644 --- a/src/components/YDBDefinitionList/YDBDefinitionList.tsx +++ b/src/components/YDBDefinitionList/YDBDefinitionList.tsx @@ -1,7 +1,7 @@ import React from 'react'; -import type {DefinitionListProps} from '@gravity-ui/uikit'; -import {DefinitionList} from '@gravity-ui/uikit'; +import type {DefinitionListProps, IconData, LabelProps} from '@gravity-ui/uikit'; +import {DefinitionList, Flex, Icon, Label} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; @@ -11,26 +11,91 @@ import './YDBDefinitionList.scss'; const b = cn('ydb-definition-list'); -export type YDBDefinitionListItem = {name: string; content: React.ReactNode; copyText?: string}; +export type YDBDefinitionListItem = { + name: string; + content: React.ReactNode; + copyText?: string; +}; + +export interface YDBDefinitionListHeaderLabel { + id: string; + value: React.ReactNode; + icon?: IconData; + theme?: LabelProps['theme']; +} interface YDBDefinitionListProps extends Omit { title?: React.ReactNode; + titleSuffix?: React.ReactNode; + titleSeparator?: React.ReactNode; items: YDBDefinitionListItem[]; + headerLabels?: YDBDefinitionListHeaderLabel[]; + iconSize?: number; + labelSize?: LabelProps['size']; + footer?: React.ReactNode; + compact?: boolean; } /** DefinitionList with predefined styles and layout */ export function YDBDefinitionList({ title, + titleSuffix, + titleSeparator = '•', + headerLabels, + iconSize = 12, + labelSize = 'xs', + footer, items, + compact, nameMaxWidth = 220, className, ...restProps }: YDBDefinitionListProps) { - const renderTitle = () => { - if (title) { - return
{title}
; + const hasHeader = Boolean(title || titleSuffix || (headerLabels && headerLabels.length)); + + const renderHeader = () => { + if (!hasHeader) { + return null; } - return null; + + return ( + + {title && ( + +
{title}
+ {titleSuffix && ( + +
{titleSeparator}
+
{titleSuffix}
+
+ )} +
+ )} + {headerLabels && headerLabels.length > 0 && ( + + {headerLabels.map((label) => ( + + ))} + + )} +
+ ); }; const renderContent = () => { @@ -51,10 +116,19 @@ export function YDBDefinitionList({ return i18n('no-data'); }; + const renderFooter = () => { + if (!footer) { + return null; + } + + return
{footer}
; + }; + return ( -
- {renderTitle()} +
+ {renderHeader()} {renderContent()} + {renderFooter()}
); } diff --git a/src/containers/Storage/Disks/Disks.scss b/src/containers/Storage/Disks/Disks.scss index facc5de46d..4bb544e9df 100644 --- a/src/containers/Storage/Disks/Disks.scss +++ b/src/containers/Storage/Disks/Disks.scss @@ -38,7 +38,6 @@ } &__pdisk-progress-bar { --progress-bar-full-height: 20px; - padding-left: var(--g-spacing-2); text-align: left; } diff --git a/src/containers/Storage/Disks/Disks.tsx b/src/containers/Storage/Disks/Disks.tsx index 6090a20869..66c2243c07 100644 --- a/src/containers/Storage/Disks/Disks.tsx +++ b/src/containers/Storage/Disks/Disks.tsx @@ -22,9 +22,10 @@ interface DisksProps { vDisks?: PreparedVDisk[]; viewContext?: StorageViewContext; erasure?: Erasure; + withIcon?: boolean; } -export function Disks({vDisks = [], viewContext, erasure}: DisksProps) { +export function Disks({vDisks = [], viewContext, erasure, withIcon}: DisksProps) { const [highlightedVDisk, setHighlightedVDisk] = React.useState(); const vDisksWithDCMargins = useVDisksWithDCMargins(vDisks, erasure); @@ -51,6 +52,7 @@ export function Disks({vDisks = [], viewContext, erasure}: DisksProps) { highlightedVDisk={highlightedVDisk} setHighlightedVDisk={setHighlightedVDisk} unavailableVDiskWidth={unavailableVDiskWidth} + withIcon={withIcon} /> ))} @@ -63,6 +65,7 @@ export function Disks({vDisks = [], viewContext, erasure}: DisksProps) { highlightedVDisk={highlightedVDisk} setHighlightedVDisk={setHighlightedVDisk} withDCMargin={vDisksWithDCMargins.includes(index)} + withIcon={withIcon} /> ))}
@@ -77,6 +80,7 @@ interface DisksItemProps { setHighlightedVDisk: (id: string | undefined) => void; unavailableVDiskWidth?: number; withDCMargin?: boolean; + withIcon?: boolean; } function VDiskItem({ @@ -85,6 +89,7 @@ function VDiskItem({ inactive, setHighlightedVDisk, unavailableVDiskWidth, + withIcon, }: DisksItemProps) { // Do not show PDisk popup for VDisk const vDiskToShow = {...vDisk, PDisk: undefined}; @@ -100,6 +105,7 @@ function VDiskItem({ setHighlightedVDisk(vDiskId)} onHidePopup={() => setHighlightedVDisk(undefined)} + withIcon={withIcon} /> ); } diff --git a/src/containers/Storage/PDisk/PDisk.scss b/src/containers/Storage/PDisk/PDisk.scss index 1e45605632..5e442fd5d2 100644 --- a/src/containers/Storage/PDisk/PDisk.scss +++ b/src/containers/Storage/PDisk/PDisk.scss @@ -49,17 +49,4 @@ --ydb-stack-offset-x-hover: 0px; --ydb-stack-offset-y-hover: -7px; } - - &__media-type { - position: absolute; - top: 50%; - right: 4px; - - font-size: var(--g-text-body-1-font-size); - line-height: var(--g-text-body-1-line-height); - - color: var(--g-color-text-secondary); - - transform: translateY(-50%); - } } diff --git a/src/containers/Storage/PDisk/PDisk.tsx b/src/containers/Storage/PDisk/PDisk.tsx index 41f2b4bdf2..b79e72095a 100644 --- a/src/containers/Storage/PDisk/PDisk.tsx +++ b/src/containers/Storage/PDisk/PDisk.tsx @@ -1,12 +1,13 @@ import React from 'react'; +import {isNil} from 'lodash'; + import {DiskStateProgressBar} from '../../../components/DiskStateProgressBar/DiskStateProgressBar'; import {HoverPopup} from '../../../components/HoverPopup/HoverPopup'; import {InternalLink} from '../../../components/InternalLink'; import {PDiskPopup} from '../../../components/PDiskPopup/PDiskPopup'; import {VDisk} from '../../../components/VDisk/VDisk'; import {getPDiskPagePath} from '../../../routes'; -import {valueIsDefined} from '../../../utils'; import {cn} from '../../../utils/cn'; import type {PreparedPDisk, PreparedVDisk} from '../../../utils/disks/types'; import {DISKS_POPUP_DEBOUNCE_TIMEOUT} from '../shared'; @@ -29,6 +30,7 @@ interface PDiskProps { width?: number; delayOpen?: number; delayClose?: number; + withIcon?: boolean; } export const PDisk = ({ @@ -43,10 +45,10 @@ export const PDisk = ({ width, delayOpen = DISKS_POPUP_DEBOUNCE_TIMEOUT, delayClose = DISKS_POPUP_DEBOUNCE_TIMEOUT, + withIcon, }: PDiskProps) => { const {NodeId, PDiskId} = data; - const pDiskIdsDefined = valueIsDefined(NodeId) && valueIsDefined(PDiskId); - + const pDiskIdsDefined = !isNil(NodeId) && !isNil(PDiskId); const anchorRef = React.useRef(null); const renderVDisks = () => { @@ -67,6 +69,7 @@ export const PDisk = ({ }} > -
{data.Type}
diff --git a/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx b/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx index ca7bc3baab..1a4c932c58 100644 --- a/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx +++ b/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx @@ -1,13 +1,13 @@ import {ShieldKeyhole} from '@gravity-ui/icons'; import DataTable from '@gravity-ui/react-data-table'; import {Flex, Icon, Label, Popover} from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; import {CellWithPopover} from '../../../../components/CellWithPopover/CellWithPopover'; import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus'; import {StatusIcon} from '../../../../components/StatusIcon/StatusIcon'; import {UsageLabel} from '../../../../components/UsageLabel/UsageLabel'; import {useStorageGroupPath} from '../../../../routes'; -import {valueIsDefined} from '../../../../utils'; import {cn} from '../../../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER, YDB_POPOVER_CLASS_NAME} from '../../../../utils/constants'; import {formatNumber} from '../../../../utils/dataFormatters/dataFormatters'; @@ -123,7 +123,7 @@ const usageColumn: StorageGroupsColumn = { width: 85, resizeMinWidth: 75, render: ({row}) => { - return valueIsDefined(row.Usage) ? ( + return !isNil(row.Usage) ? ( ) : ( EMPTY_DATA_PLACEHOLDER @@ -137,7 +137,7 @@ const diskSpaceUsageColumn: StorageGroupsColumn = { width: 115, resizeMinWidth: 75, render: ({row}) => { - return valueIsDefined(row.DiskSpaceUsage) ? ( + return !isNil(row.DiskSpaceUsage) ? ( { - return valueIsDefined(row.LatencyPutTabletLogMs) + return !isNil(row.LatencyPutTabletLogMs) ? formatToMs(row.LatencyPutTabletLogMs) : EMPTY_DATA_PLACEHOLDER; }, @@ -227,7 +227,7 @@ const allocationUnitsColumn: StorageGroupsColumn = { header: STORAGE_GROUPS_COLUMNS_TITLES.AllocationUnits, width: 150, render: ({row}) => { - return valueIsDefined(row.AllocationUnits) + return !isNil(row.AllocationUnits) ? formatNumber(row.AllocationUnits) : EMPTY_DATA_PLACEHOLDER; }, @@ -239,7 +239,12 @@ const getVDisksColumn = (data?: GetStorageColumnsData): StorageGroupsColumn => ( header: STORAGE_GROUPS_COLUMNS_TITLES.VDisks, className: b('vdisks-column'), render: ({row}) => ( - + ), align: DataTable.CENTER, width: 780, // usually 8-9 vdisks, this width corresponds to 8 vdisks, column is expanded if more @@ -257,6 +262,7 @@ const getDisksColumn = (data?: GetStorageColumnsData): StorageGroupsColumn => ({ vDisks={row.VDisks} viewContext={data?.viewContext} erasure={row.ErasureSpecies} + withIcon /> ); }, diff --git a/src/containers/Storage/VDisks/VDisks.tsx b/src/containers/Storage/VDisks/VDisks.tsx index 243fb1193c..b268cbd598 100644 --- a/src/containers/Storage/VDisks/VDisks.tsx +++ b/src/containers/Storage/VDisks/VDisks.tsx @@ -14,15 +14,17 @@ interface VDisksProps { vDisks?: PreparedVDisk[]; viewContext?: StorageViewContext; erasure?: Erasure; + withIcon?: boolean; } -export function VDisks({vDisks, viewContext, erasure}: VDisksProps) { +export function VDisks({vDisks, viewContext, erasure, withIcon}: VDisksProps) { const vDisksWithDCMargins = useVDisksWithDCMargins(vDisks, erasure); return (
{vDisks?.map((vDisk, index) => ( 3 parameters instead of 5 +const makeVDiskLocationKey = ( + nodeId?: number, + pDiskId?: number, + vDiskSlotId?: number, +): string | undefined => { + if (isNil(nodeId) || isNil(pDiskId) || isNil(vDiskSlotId)) { + return undefined; + } + + return stringifyVdiskId({ + NodeId: nodeId, + PDiskId: pDiskId, + VSlotId: vDiskSlotId, + }); +}; + +// Attaches recipient references to donor VDisks based on their Donors relations +const attachRecipientsToDonors = (nodes: PreparedStorageNode[] | undefined) => { + if (!nodes?.length) { + return; + } + + const vdiskByLocation = new Map(); + + nodes.forEach((node) => { + node.VDisks?.forEach((vdisk) => { + const key = makeVDiskLocationKey(vdisk.NodeId, vdisk.PDiskId, vdisk.VDiskSlotId); + + if (key) { + vdiskByLocation.set(key, vdisk); + } + }); + }); + + nodes.forEach((node) => { + node.VDisks?.forEach((replication) => { + if (replication.Replicated || !replication.Donors?.length) { + return; + } + + replication.Donors.forEach((donorRef) => { + const key = makeVDiskLocationKey( + donorRef.NodeId, + donorRef.PDiskId, + donorRef.VDiskSlotId, + ); + + if (!key) { + return; + } + + const donor = vdiskByLocation.get(key); + if (!donor) { + return; + } + + donor.Recipient = { + NodeId: replication.NodeId, + StringifiedId: replication.StringifiedId, + }; + + // Keep the Donors item in sync with the real donor VDisk: reuse its StringifiedId + // instead of the local slot-based id + if (donorRef.StringifiedId !== donor.StringifiedId) { + donorRef.StringifiedId = donor.StringifiedId; + } + }); + }); + }); +}; + // ==== Prepare responses ==== export const prepareStorageNodesResponse = (data: TNodesInfo): PreparedStorageResponse => { @@ -297,6 +374,8 @@ export const prepareStorageNodesResponse = (data: TNodesInfo): PreparedStorageRe prepareStorageNodeData(node, maxSlotsPerDisk, maxDisksPerNode), ); + attachRecipientsToDonors(preparedNodes); + return { nodes: preparedNodes, total: Number(TotalNodes) || preparedNodes?.length, diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss index 5ccd333d18..2d1f18a825 100644 --- a/src/styles/mixins.scss +++ b/src/styles/mixins.scss @@ -256,6 +256,7 @@ &_blue { --entity-state-font-color: var(--g-color-text-info); --entity-state-border-color: var(--g-color-base-info-heavy); + --entity-state-shadow-color: var(--g-color-base-info-light); --entity-state-background-color: var(--g-color-base-info-light); --entity-state-fill-color: var(--g-color-base-info-medium); } @@ -267,18 +268,21 @@ --entity-state-fill-color: var(--g-color-base-yellow-medium); } - &_orange { - --entity-state-font-color: var(--g-color-private-orange-500); - --entity-state-border-color: var(--ydb-color-status-orange); - --entity-state-background-color: var(--g-color-private-orange-100); - --entity-state-fill-color: var(--g-color-private-orange-300); - } &_red { --entity-state-font-color: var(--g-color-text-danger); --entity-state-border-color: var(--g-color-base-danger-heavy); --entity-state-background-color: var(--g-color-base-danger-light); --entity-state-fill-color: var(--g-color-base-danger-medium); } + + &_darkgrey { + --entity-state-font-color: var(--g-color-text-secondary); + --entity-state-border-color: var(--g-color-base-neutral-heavy); + --entity-state-shadow-color: var(--g-color-base-neutral-light); + --entity-state-fill-color: var(--g-color-base-neutral-light); + --entity-state-background-color: transparent; + } + &__grey { --entity-state-font-color: var(--g-color-text-secondary); --entity-state-border-color: var(--g-color-line-generic-hover); diff --git a/src/utils/disks/__test__/calculateVDiskSeverity.test.ts b/src/utils/disks/__test__/calculateVDiskSeverity.test.ts index 33a887d1d8..c1a392e9e7 100644 --- a/src/utils/disks/__test__/calculateVDiskSeverity.test.ts +++ b/src/utils/disks/__test__/calculateVDiskSeverity.test.ts @@ -23,7 +23,7 @@ describe('VDisk state', () => { expect(severity1).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow); expect(severity2).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red); - expect(severity3).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Orange); + expect(severity3).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow); }); test('Should not pick the highest severity based on FrontQueues value', () => { @@ -39,7 +39,7 @@ describe('VDisk state', () => { }); expect(severity1).not.toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red); - expect(severity2).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red); + expect(severity2).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow); }); // prettier-ignore diff --git a/src/utils/disks/calculatePDiskSeverity.ts b/src/utils/disks/calculatePDiskSeverity.ts index ecde637ba4..440fbd387d 100644 --- a/src/utils/disks/calculatePDiskSeverity.ts +++ b/src/utils/disks/calculatePDiskSeverity.ts @@ -19,7 +19,7 @@ export function calculatePDiskSeverity< return Math.max(stateSeverity, spaceSeverity); } -function getStateSeverity(pDiskState?: TPDiskState) { +export function getStateSeverity(pDiskState?: TPDiskState) { return isSeverityKey(pDiskState) ? PDISK_STATE_SEVERITY[pDiskState] : NOT_AVAILABLE_SEVERITY; } diff --git a/src/utils/disks/calculateVDiskSeverity.ts b/src/utils/disks/calculateVDiskSeverity.ts index cdfa5cd33c..efc07a7b5a 100644 --- a/src/utils/disks/calculateVDiskSeverity.ts +++ b/src/utils/disks/calculateVDiskSeverity.ts @@ -13,6 +13,7 @@ export function calculateVDiskSeverity< VDiskState?: EVDiskState; FrontQueues?: EFlag; Replicated?: boolean; + DonorMode?: boolean; }, >(vDisk: T) { const {DiskSpace, VDiskState, FrontQueues, Replicated} = vDisk; @@ -22,10 +23,13 @@ export function calculateVDiskSeverity< return NOT_AVAILABLE_SEVERITY; } - const DiskSpaceSeverity = getColorSeverity(DiskSpace); + const DiskSpaceSeverity = Math.min( + DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow, + getColorSeverity(DiskSpace), + ); const VDiskSpaceSeverity = getStateSeverity(VDiskState); const FrontQueuesSeverity = Math.min( - DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Orange, + DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow, getColorSeverity(FrontQueues), ); @@ -39,7 +43,7 @@ export function calculateVDiskSeverity< return severity; } -function getStateSeverity(vDiskState?: EVDiskState) { +export function getStateSeverity(vDiskState?: EVDiskState) { if (!vDiskState) { return NOT_AVAILABLE_SEVERITY; } @@ -52,7 +56,7 @@ function getColorSeverity(color?: EFlag) { return NOT_AVAILABLE_SEVERITY; } - // Blue is reserved for not replicated VDisks + // Blue is reserved for not replicated VDisks. DarkGrey is reserved for donors. if (color === EFlag.Blue) { return DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green; } diff --git a/src/utils/disks/constants.ts b/src/utils/disks/constants.ts index fcbd53e320..a8d367cc50 100644 --- a/src/utils/disks/constants.ts +++ b/src/utils/disks/constants.ts @@ -1,3 +1,13 @@ +import { + ArrowsRotateLeft, + BucketPaint, + CircleCheck, + CircleExclamation, + CircleExclamationFill, + TriangleExclamation, +} from '@gravity-ui/icons'; +import type {IconData, LabelProps} from '@gravity-ui/uikit'; + import type {EFlag} from '../../types/api/enums'; import {TPDiskState} from '../../types/api/pdisk'; import {EVDiskState} from '../../types/api/vdisk'; @@ -12,6 +22,8 @@ export const DISK_COLOR_STATE_TO_NUMERIC_SEVERITY: Record = { Red: 5, } as const; +export const DONOR_COLOR = 'DarkGrey'; + type SeverityToColor = Record; export const DISK_NUMERIC_SEVERITY_TO_STATE_COLOR = Object.entries( @@ -52,3 +64,45 @@ export const PDISK_STATE_SEVERITY = { [TPDiskState.DeviceIoError]: DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red, [TPDiskState.Stopped]: DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red, }; + +export interface LabelVisualConfig { + theme: LabelProps['theme']; + icon?: IconData; +} + +export const NUMERIC_SEVERITY_TO_LABEL_VIEW: Record = { + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green]: { + theme: 'success', + icon: CircleCheck, + }, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow]: { + theme: 'warning', + icon: TriangleExclamation, + }, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red]: { + theme: 'danger', + icon: CircleExclamation, + }, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue]: { + theme: 'info', + icon: ArrowsRotateLeft, + }, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Grey]: { + theme: 'normal', + }, +}; + +export const DONOR_ICON: IconData = BucketPaint; +export const DISPLAYED_DISK_ERROR_ICON: IconData = CircleExclamationFill; +export const DONOR_THEME: LabelProps['theme'] = 'unknown'; + +export const VDISK_LABEL_CONFIG: Record = { + donor: { + theme: DONOR_THEME, + icon: DONOR_ICON, + }, + replica: { + theme: NUMERIC_SEVERITY_TO_LABEL_VIEW[DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue].theme, + icon: NUMERIC_SEVERITY_TO_LABEL_VIEW[DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue].icon, + }, +}; diff --git a/src/utils/disks/helpers.ts b/src/utils/disks/helpers.ts index da39644c45..d38e4c7f57 100644 --- a/src/utils/disks/helpers.ts +++ b/src/utils/disks/helpers.ts @@ -1,4 +1,6 @@ -import {valueIsDefined} from '..'; +import type {IconData} from '@gravity-ui/uikit'; +import {isNil} from 'lodash'; + import type {VDiskBlobIndexStatParams} from '../../store/reducers/vdisk/vdisk'; import {EFlag} from '../../types/api/enums'; import type {TVDiskStateInfo, TVSlotId} from '../../types/api/vdisk'; @@ -7,6 +9,8 @@ import {generateEvaluator} from '../generateEvaluator'; import { DISK_COLOR_STATE_TO_NUMERIC_SEVERITY, DISK_NUMERIC_SEVERITY_TO_STATE_COLOR, + DISPLAYED_DISK_ERROR_ICON, + DONOR_ICON, NOT_AVAILABLE_SEVERITY_COLOR, } from './constants'; import type {PreparedVDisk} from './types'; @@ -20,7 +24,7 @@ export function isFullVDiskData( const getSpaceFlag = generateEvaluator([EFlag.Green, EFlag.Yellow, EFlag.Red]); export const getSpaceSeverity = (allocatedPercent?: number) => { - return valueIsDefined(allocatedPercent) ? getColorSeverity(getSpaceFlag(allocatedPercent)) : 0; + return !isNil(allocatedPercent) ? getColorSeverity(getSpaceFlag(allocatedPercent)) : 0; }; export function getSeverityColor(severity: number | undefined) { @@ -42,7 +46,7 @@ export function getPDiskId({ nodeId?: string | number | null; pDiskId?: string | number | null; }) { - if (valueIsDefined(nodeId) && valueIsDefined(pDiskId)) { + if (!isNil(nodeId) && !isNil(pDiskId)) { return `${nodeId}-${pDiskId}`; } return undefined; @@ -55,3 +59,23 @@ export function getVDiskId(params: VDiskBlobIndexStatParams) { : [params.nodeId, params.pDiskId, params.vDiskSlotId]; return parts.join('-'); } + +export function getVDiskStatusIcon(severity?: number, isDonor?: boolean): IconData | undefined { + if (severity === undefined) { + return undefined; + } + + const isError = severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red; + const isReplicating = severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue; + + // Display icon only for error and donor + if (isReplicating && isDonor) { + return DONOR_ICON; + } + + if (isError) { + return DISPLAYED_DISK_ERROR_ICON; + } + + return undefined; +} diff --git a/src/utils/disks/prepareDisks.ts b/src/utils/disks/prepareDisks.ts index 6770b0d80c..11b41b29b3 100644 --- a/src/utils/disks/prepareDisks.ts +++ b/src/utils/disks/prepareDisks.ts @@ -1,6 +1,5 @@ import {isNil} from 'lodash'; -import {valueIsDefined} from '..'; import type {TPDiskStateInfo} from '../../types/api/pdisk'; import type {TVDiskStateInfo, TVSlotId} from '../../types/api/vdisk'; import {stringifyVdiskId} from '../dataFormatters/dataFormatters'; @@ -18,7 +17,7 @@ export function prepareWhiteboardVDiskData( const {NodeId, PDiskId, VSlotId} = vDiskState; const vDiskId = - valueIsDefined(VSlotId) && valueIsDefined(PDiskId) && valueIsDefined(NodeId) + !isNil(VSlotId) && !isNil(PDiskId) && !isNil(NodeId) ? { NodeId, PDiskId, diff --git a/src/utils/disks/types.ts b/src/utils/disks/types.ts index 28b4a8fbe3..841414700a 100644 --- a/src/utils/disks/types.ts +++ b/src/utils/disks/types.ts @@ -21,6 +21,11 @@ export type PreparedPDisk = Omit< SlotSize?: string; }; +export interface VDiskRecipientRef { + NodeId?: number; + StringifiedId?: string; +} + export interface PreparedVDisk extends Omit { PDisk?: PreparedPDisk; @@ -33,6 +38,8 @@ export interface PreparedVDisk SizeLimit?: number; Donors?: PreparedVDisk[]; + + Recipient?: VDiskRecipientRef; } export type PDiskType = ValueOf;