From e830ca54352177ac14639175eadf00129bbfa1a0 Mon Sep 17 00:00:00 2001 From: Daria Vorontsova Date: Wed, 19 Nov 2025 19:34:23 +0300 Subject: [PATCH 01/16] feat(storage): update vdisk donor/replica visuals and tooltips --- .../DiskStateProgressBar.scss | 18 +++ .../DiskStateProgressBar.tsx | 13 +- src/components/InfoViewer/InfoViewer.scss | 8 +- src/components/InfoViewer/InfoViewer.tsx | 54 +++++++- src/components/PDiskPopup/PDiskPopup.scss | 5 + src/components/PDiskPopup/PDiskPopup.tsx | 37 +++--- src/components/VDisk/VDisk.scss | 6 + src/components/VDisk/VDisk.tsx | 24 ++++ src/components/VDiskPopup/VDiskPopup.scss | 4 + src/components/VDiskPopup/VDiskPopup.tsx | 124 ++++++++++++++---- src/components/VDiskPopup/i18n/en.json | 7 +- src/store/reducers/storage/utils.ts | 76 +++++++++++ src/styles/mixins.scss | 6 +- src/utils/disks/calculateVDiskSeverity.ts | 5 +- src/utils/disks/types.ts | 7 + 15 files changed, 338 insertions(+), 56 deletions(-) create mode 100644 src/components/PDiskPopup/PDiskPopup.scss diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss index c9b916b643..5a6d29795c 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss @@ -45,6 +45,14 @@ background-color: unset; } + &_striped { + overflow: hidden; + + // Inset shadow = border overlay without shrinking the striped fill + border: none; + box-shadow: 0 0 0 $border-width var(--entity-state-shadow-color) inset; + } + &__fill-bar { position: absolute; top: 0; @@ -70,6 +78,16 @@ border-radius: 0 $inner-border-radius $inner-border-radius 0; } + + &_striped { + background-image: repeating-linear-gradient( + 135deg, + transparent 0, + transparent 4px, + var(--entity-state-fill-color) 4px, + var(--entity-state-fill-color) 8px + ); + } } &__title { diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx index e5e4252734..b9d42a853a 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx @@ -16,6 +16,7 @@ interface DiskStateProgressBarProps { faded?: boolean; inactive?: boolean; empty?: boolean; + striped?: boolean; content?: React.ReactNode; className?: string; } @@ -28,15 +29,23 @@ export function DiskStateProgressBar({ inactive, empty, content, + striped, className, }: 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; + mods[color.toLowerCase()] = true; } const renderAllocatedPercent = () => { diff --git a/src/components/InfoViewer/InfoViewer.scss b/src/components/InfoViewer/InfoViewer.scss index c75126ee02..9ce3699fdf 100644 --- a/src/components/InfoViewer/InfoViewer.scss +++ b/src/components/InfoViewer/InfoViewer.scss @@ -10,12 +10,18 @@ font-size: var(--ydb-info-viewer-font-size); line-height: var(--ydb-info-viewer-line-height); - &__title { + &__header { margin: var(--ydb-info-viewer-title-margin); + } + &__title { font-weight: var(--ydb-info-viewer-title-font-weight); } + &__title-suffix { + color: var(--g-color-text-secondary); + } + &__items { display: flex; flex-direction: column; diff --git a/src/components/InfoViewer/InfoViewer.tsx b/src/components/InfoViewer/InfoViewer.tsx index 248b8037b3..a357fe53c7 100644 --- a/src/components/InfoViewer/InfoViewer.tsx +++ b/src/components/InfoViewer/InfoViewer.tsx @@ -1,5 +1,8 @@ import React from 'react'; +import type {IconData, LabelProps} from '@gravity-ui/uikit'; +import {Flex, Icon, Label} from '@gravity-ui/uikit'; + import {cn} from '../../utils/cn'; import i18n from './i18n'; @@ -13,6 +16,8 @@ export interface InfoViewerItem { export interface InfoViewerProps { title?: React.ReactNode; + titleSuffix?: React.ReactNode; + separator?: React.ReactNode; info?: InfoViewerItem[]; dots?: boolean; size?: 's'; @@ -20,12 +25,18 @@ export interface InfoViewerProps { className?: string; multilineLabels?: boolean; renderEmptyState?: (props?: Pick) => React.ReactNode; + showLabel?: boolean; + labelText?: string; + labelIcon?: IconData; + labelTheme?: LabelProps['theme']; } const b = cn('info-viewer'); export const InfoViewer = ({ title, + titleSuffix, + separator = '•', info, dots = true, size, @@ -33,6 +44,10 @@ export const InfoViewer = ({ className, multilineLabels, renderEmptyState, + showLabel, + labelText, + labelIcon, + labelTheme, }: InfoViewerProps) => { if ((!info || !info.length) && renderEmptyState) { return {renderEmptyState({title, size})}; @@ -40,17 +55,44 @@ export const InfoViewer = ({ return (
- {title &&
{title}
} + + {title && ( + +
{title}
+ {titleSuffix && ( + +
{separator}
+
{titleSuffix}
+
+ )} +
+ )} + {showLabel && ( + + )} +
{info && info.length > 0 ? (
{info.map((data, infoIndex) => (
-
-
- {data.label} + {data.label && ( +
+
+ {data.label} +
+ {dots &&
}
- {dots &&
} -
+ )}
{data.value}
diff --git a/src/components/PDiskPopup/PDiskPopup.scss b/src/components/PDiskPopup/PDiskPopup.scss new file mode 100644 index 0000000000..20d960358d --- /dev/null +++ b/src/components/PDiskPopup/PDiskPopup.scss @@ -0,0 +1,5 @@ +.pdisk-storage-popup { + &__links { + margin-top: 4px; + } +} diff --git a/src/components/PDiskPopup/PDiskPopup.tsx b/src/components/PDiskPopup/PDiskPopup.tsx index 9bf2493369..6804323a3c 100644 --- a/src/components/PDiskPopup/PDiskPopup.tsx +++ b/src/components/PDiskPopup/PDiskPopup.tsx @@ -5,6 +5,7 @@ import {Flex} from '@gravity-ui/uikit'; import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; import {valueIsDefined} from '../../utils'; +import {cn} from '../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import {createPDiskDeveloperUILink} from '../../utils/developerUI/developerUI'; import type {PreparedPDisk} from '../../utils/disks/types'; @@ -18,6 +19,10 @@ import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; import {pDiskInfoKeyset} from '../PDiskInfo/i18n'; import {PDiskPageLink} from '../PDiskPageLink/PDiskPageLink'; +import './PDiskPopup.scss'; + +const b = cn('pdisk-storage-popup'); + const errorColors = [EFlag.Orange, EFlag.Red, EFlag.Yellow]; export const preparePDiskData = ( @@ -25,24 +30,9 @@ export const preparePDiskData = ( nodeData?: {Host?: string; DC?: string}, withDeveloperUILink?: boolean, ) => { - const { - AvailableSize, - TotalSize, - State, - PDiskId, - NodeId, - StringifiedId, - Path, - Realtime, - Type, - Device, - } = data; + const {AvailableSize, TotalSize, State, PDiskId, NodeId, 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'}, ]; @@ -84,9 +74,9 @@ export const preparePDiskData = ( }); pdiskData.push({ - label: 'Links', + label: null, value: ( - + { [data, nodeData, isUserAllowedToMakeChanges], ); - return ; + const pdiskId = data.StringifiedId; + + return ( + + ); }; diff --git a/src/components/VDisk/VDisk.scss b/src/components/VDisk/VDisk.scss index 4e76ac289e..437ab72bba 100644 --- a/src/components/VDisk/VDisk.scss +++ b/src/components/VDisk/VDisk.scss @@ -6,4 +6,10 @@ border-radius: 4px; // to match interactive area with disk shape } + + &__donor-icon { + position: absolute; + top: 3px; + left: 4px; + } } diff --git a/src/components/VDisk/VDisk.tsx b/src/components/VDisk/VDisk.tsx index 0459bf816b..e49f3c6a22 100644 --- a/src/components/VDisk/VDisk.tsx +++ b/src/components/VDisk/VDisk.tsx @@ -1,4 +1,9 @@ +import {BucketPaint} from '@gravity-ui/icons'; +import {Icon} from '@gravity-ui/uikit'; + +import {useStorageQueryParams} from '../../containers/Storage/useStorageQueryParams'; import {useVDiskPagePath} from '../../routes'; +import {STORAGE_TYPES} from '../../store/reducers/storage/constants'; import {cn} from '../../utils/cn'; import type {PreparedVDisk} from '../../utils/disks/types'; import {DiskStateProgressBar} from '../DiskStateProgressBar/DiskStateProgressBar'; @@ -33,9 +38,19 @@ export const VDisk = ({ delayClose, delayOpen, }: VDiskProps) => { + const {storageType} = useStorageQueryParams(); + const isGroupView = storageType === STORAGE_TYPES.groups; + const getVDiskLink = useVDiskPagePath(); const vDiskPath = getVDiskLink({nodeId: data.NodeId, vDiskId: data.StringifiedId}); + // Donor and replicating Vdisks have similar data.replicated and data.VDiskState params + const isNotReplicating = data.Replicated === false && data.VDiskState === 'OK'; + // The difference is only in data.donorMode + const isDonor = data.DonorMode; + + const isDonorIconShow = isGroupView && isDonor; + return ( + +
+ ) : null + } />
diff --git a/src/components/VDiskPopup/VDiskPopup.scss b/src/components/VDiskPopup/VDiskPopup.scss index c98fcc9fcc..55155675f2 100644 --- a/src/components/VDiskPopup/VDiskPopup.scss +++ b/src/components/VDiskPopup/VDiskPopup.scss @@ -9,4 +9,8 @@ &__donor-label { margin-bottom: 8px; } + + &__links { + margin-top: 4px; + } } diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 7e61bb28a4..8698b53465 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -1,6 +1,8 @@ import React from 'react'; -import {Flex, Label} from '@gravity-ui/uikit'; +import {ArrowsRotateLeft, BucketPaint} from '@gravity-ui/icons'; +import type {IconData, LabelProps} from '@gravity-ui/uikit'; +import {Flex} from '@gravity-ui/uikit'; import {useVDiskPagePath} from '../../routes'; import {selectNodesMap} from '../../store/reducers/nodesList'; @@ -63,8 +65,12 @@ const prepareUnavailableVDiskData = (data: UnavailableDonor, withDeveloperUILink }); vdiskData.push({ - label: vDiskPopupKeyset('label_links'), - value: , + label: null, + value: ( +
+ +
+ ), }); } @@ -97,10 +103,12 @@ const prepareVDiskData = ( ReadThroughput, WriteThroughput, StoragePoolName, + Donors, + DonorMode, + Recipient, } = data; const vdiskData: InfoViewerItem[] = [ - {label: vDiskPopupKeyset('label_vdisk'), value: StringifiedId}, { label: vDiskPopupKeyset('label_state'), value: VDiskState ?? vDiskPopupKeyset('context_not-available'), @@ -111,6 +119,61 @@ const prepareVDiskData = ( vdiskData.push({label: vDiskPopupKeyset('label_storage-pool'), value: StoragePoolName}); } + if (Donors?.length && getVDiskLinkFn) { + vdiskData.push({ + label: vDiskPopupKeyset('label_donors'), + value: ( + + {Donors.map((donor) => { + if (!valueIsDefined(donor.NodeId) || !valueIsDefined(donor.StringifiedId)) { + return ( +
+ {donor.StringifiedId ?? EMPTY_DATA_PLACEHOLDER} +
+ ); + } + + return ( + + {vDiskPopupKeyset('label_vdisk')} {donor.StringifiedId} + + ); + })} +
+ ), + }); + } + + if (DonorMode && Recipient && getVDiskLinkFn) { + let recipientContent: React.ReactNode; + + if (valueIsDefined(Recipient.NodeId) && valueIsDefined(Recipient.StringifiedId)) { + const recipientPath = getVDiskLinkFn({ + nodeId: Recipient.NodeId, + vDiskId: Recipient.StringifiedId, + }); + + recipientContent = recipientPath ? ( + + {vDiskPopupKeyset('label_vdisk')} {Recipient.StringifiedId} + + ) : ( +
{Recipient.StringifiedId}
+ ); + } + + vdiskData.push({ + label: vDiskPopupKeyset('label_recipient'), + value: recipientContent, + }); + } + if (SatisfactionRank && SatisfactionRank.FreshRank?.Flag !== EFlag.Green) { vdiskData.push({ label: vDiskPopupKeyset('label_fresh'), @@ -212,9 +275,9 @@ const prepareVDiskData = ( const vDiskPagePath = getVDiskLinkFn?.({nodeId: NodeId, vDiskId: StringifiedId}); if (vDiskPagePath) { vdiskData.push({ - label: vDiskPopupKeyset('label_links'), + label: null, value: ( - + { [data, nodeData, 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 = isFullVDiskData(data) ? data.StringifiedId : undefined; + + const labelConfig: { + showLabel: boolean; + labelText?: string; + labelIcon?: IconData; + labelTheme?: LabelProps['theme']; + } = { + showLabel: false, + }; + + if (data.DonorMode) { + labelConfig.showLabel = true; + labelConfig.labelText = vDiskPopupKeyset('label_donor'); + labelConfig.labelIcon = BucketPaint; + labelConfig.labelTheme = 'unknown'; + } else if (isFullVDiskData(data) && !data.Replicated) { + labelConfig.showLabel = true; + labelConfig.labelText = vDiskPopupKeyset('label_replication'); + labelConfig.labelIcon = ArrowsRotateLeft; + labelConfig.labelTheme = 'info'; } return (
- {data.DonorMode && } - + {pdiskInfo && isViewerUser && } - {donorsInfo.length > 0 && }
); }; diff --git a/src/components/VDiskPopup/i18n/en.json b/src/components/VDiskPopup/i18n/en.json index 4d82fa9a17..07b555b095 100644 --- a/src/components/VDiskPopup/i18n/en.json +++ b/src/components/VDiskPopup/i18n/en.json @@ -2,6 +2,7 @@ "context_not-available": "not available", "label_state": "State", "label_storage-pool": "StoragePool", + "label_donors": "Donors", "label_node-id": "NodeId", "label_pdisk-id": "PDiskId", "label_vslot-id": "VSlotId", @@ -17,5 +18,9 @@ "label_unsync-vdisks": "UnsyncVDisks", "label_allocated": "Allocated", "label_read": "Read", - "label_write": "Write" + "label_write": "Write", + + "label_donor": "Donor", + "label_replication": "Replication", + "label_recipient": "Recipient" } diff --git a/src/store/reducers/storage/utils.ts b/src/store/reducers/storage/utils.ts index 339f95e832..efb4aa8e0b 100644 --- a/src/store/reducers/storage/utils.ts +++ b/src/store/reducers/storage/utils.ts @@ -11,11 +11,14 @@ import type { } from '../../../types/api/storage'; import {EVDiskState} from '../../../types/api/vdisk'; import type {TVDiskStateInfo} from '../../../types/api/vdisk'; +import {valueIsDefined} from '../../../utils'; +import {stringifyVdiskId} from '../../../utils/dataFormatters/dataFormatters'; import {getColorSeverity, getSeverityColor} from '../../../utils/disks/helpers'; import { prepareWhiteboardPDiskData, prepareWhiteboardVDiskData, } from '../../../utils/disks/prepareDisks'; +import type {PreparedVDisk} from '../../../utils/disks/types'; import {prepareNodeSystemState} from '../../../utils/nodes'; import {getUsage} from '../../../utils/storage'; import {parseUsToMs} from '../../../utils/timeParsers'; @@ -275,6 +278,77 @@ export const calculateMaximumDisksPerNode = ( return maxDisks; }; +// We need custom key (can't use StringifiedId) because for array of donors in replication's object +// we have only nodeId, pDiskId, vDiskSlotId -> 3 parameters instead of 5 +const makeVDiskLocationKey = ( + nodeId?: number, + pDiskId?: number, + vDiskSlotId?: number, +): string | undefined => { + if (!valueIsDefined(nodeId) || !valueIsDefined(pDiskId) || !valueIsDefined(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. + donorRef.StringifiedId = donor.StringifiedId; + }); + }); + }); +}; + // ==== Prepare responses ==== export const prepareStorageNodesResponse = (data: TNodesInfo): PreparedStorageResponse => { @@ -297,6 +371,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..c1bbbe723e 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); } @@ -279,8 +280,11 @@ --entity-state-background-color: var(--g-color-base-danger-light); --entity-state-fill-color: var(--g-color-base-danger-medium); } - &__grey { + &_grey { --entity-state-font-color: var(--g-color-text-secondary); --entity-state-border-color: var(--g-color-line-generic-hover); + --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; } } diff --git a/src/utils/disks/calculateVDiskSeverity.ts b/src/utils/disks/calculateVDiskSeverity.ts index cdfa5cd33c..cf6f6deda1 100644 --- a/src/utils/disks/calculateVDiskSeverity.ts +++ b/src/utils/disks/calculateVDiskSeverity.ts @@ -13,12 +13,13 @@ export function calculateVDiskSeverity< VDiskState?: EVDiskState; FrontQueues?: EFlag; Replicated?: boolean; + DonorMode?: boolean; }, >(vDisk: T) { - const {DiskSpace, VDiskState, FrontQueues, Replicated} = vDisk; + const {DiskSpace, VDiskState, FrontQueues, Replicated, DonorMode} = vDisk; // if the disk is not available, this determines its status severity regardless of other features - if (!VDiskState) { + if (!VDiskState || DonorMode) { return NOT_AVAILABLE_SEVERITY; } 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; From 9d03b8d1c614cd45a7a8cff6bd1e968b19385893 Mon Sep 17 00:00:00 2001 From: Daria Vorontsova Date: Thu, 20 Nov 2025 11:52:55 +0300 Subject: [PATCH 02/16] Fix bugs --- src/components/VDiskPopup/VDiskPopup.tsx | 2 +- src/components/VDiskPopup/i18n/en.json | 4 +--- src/store/reducers/storage/utils.ts | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 8698b53465..937c910c20 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -121,7 +121,7 @@ const prepareVDiskData = ( if (Donors?.length && getVDiskLinkFn) { vdiskData.push({ - label: vDiskPopupKeyset('label_donors'), + label: vDiskPopupKeyset('label_donor'), value: ( {Donors.map((donor) => { diff --git a/src/components/VDiskPopup/i18n/en.json b/src/components/VDiskPopup/i18n/en.json index 07b555b095..0c8cf3ae80 100644 --- a/src/components/VDiskPopup/i18n/en.json +++ b/src/components/VDiskPopup/i18n/en.json @@ -2,7 +2,7 @@ "context_not-available": "not available", "label_state": "State", "label_storage-pool": "StoragePool", - "label_donors": "Donors", + "label_donor": "Donor", "label_node-id": "NodeId", "label_pdisk-id": "PDiskId", "label_vslot-id": "VSlotId", @@ -19,8 +19,6 @@ "label_allocated": "Allocated", "label_read": "Read", "label_write": "Write", - - "label_donor": "Donor", "label_replication": "Replication", "label_recipient": "Recipient" } diff --git a/src/store/reducers/storage/utils.ts b/src/store/reducers/storage/utils.ts index efb4aa8e0b..ae525b3371 100644 --- a/src/store/reducers/storage/utils.ts +++ b/src/store/reducers/storage/utils.ts @@ -279,7 +279,7 @@ export const calculateMaximumDisksPerNode = ( }; // We need custom key (can't use StringifiedId) because for array of donors in replication's object -// we have only nodeId, pDiskId, vDiskSlotId -> 3 parameters instead of 5 +// we can have only nodeId, pDiskId, vDiskSlotId -> 3 parameters instead of 5 const makeVDiskLocationKey = ( nodeId?: number, pDiskId?: number, @@ -342,7 +342,7 @@ const attachRecipientsToDonors = (nodes: PreparedStorageNode[] | undefined) => { }; // Keep the Donors item in sync with the real donor VDisk: reuse its StringifiedId - // instead of the local slot-based id. + // instead of the local slot-based id donorRef.StringifiedId = donor.StringifiedId; }); }); From 9d7726a5dcbf8e50850b7f2abc994655b1a54f2b Mon Sep 17 00:00:00 2001 From: Daria Vorontsova Date: Thu, 20 Nov 2025 16:26:20 +0300 Subject: [PATCH 03/16] Fix bugs --- src/components/InfoViewer/InfoViewer.tsx | 34 +++--- src/components/InfoViewer/index.ts | 2 +- src/components/PDiskPopup/PDiskPopup.tsx | 65 ++++++++--- src/components/PDiskPopup/i18n/en.json | 14 +++ src/components/PDiskPopup/i18n/index.ts | 7 ++ src/components/VDisk/VDisk.tsx | 24 ++-- src/components/VDiskPopup/VDiskPopup.tsx | 109 ++++++++++++------ src/components/VDiskPopup/i18n/en.json | 1 - src/styles/mixins.scss | 6 +- src/types/api/enums.ts | 1 + .../__test__/calculateVDiskSeverity.test.ts | 2 +- src/utils/disks/calculatePDiskSeverity.ts | 2 +- src/utils/disks/calculateVDiskSeverity.ts | 16 ++- src/utils/disks/constants.ts | 40 +++++++ src/utils/disks/helpers.ts | 19 +++ 15 files changed, 254 insertions(+), 88 deletions(-) create mode 100644 src/components/PDiskPopup/i18n/en.json create mode 100644 src/components/PDiskPopup/i18n/index.ts diff --git a/src/components/InfoViewer/InfoViewer.tsx b/src/components/InfoViewer/InfoViewer.tsx index a357fe53c7..36f194c993 100644 --- a/src/components/InfoViewer/InfoViewer.tsx +++ b/src/components/InfoViewer/InfoViewer.tsx @@ -14,6 +14,12 @@ export interface InfoViewerItem { value: React.ReactNode; } +export interface InfoViewerHeaderLabel { + value: React.ReactNode; + icon?: IconData; + theme?: LabelProps['theme']; +} + export interface InfoViewerProps { title?: React.ReactNode; titleSuffix?: React.ReactNode; @@ -25,10 +31,7 @@ export interface InfoViewerProps { className?: string; multilineLabels?: boolean; renderEmptyState?: (props?: Pick) => React.ReactNode; - showLabel?: boolean; - labelText?: string; - labelIcon?: IconData; - labelTheme?: LabelProps['theme']; + headerLabels?: InfoViewerHeaderLabel[]; } const b = cn('info-viewer'); @@ -44,10 +47,7 @@ export const InfoViewer = ({ className, multilineLabels, renderEmptyState, - showLabel, - labelText, - labelIcon, - labelTheme, + headerLabels, }: InfoViewerProps) => { if ((!info || !info.length) && renderEmptyState) { return {renderEmptyState({title, size})}; @@ -72,13 +72,17 @@ export const InfoViewer = ({ )} )} - {showLabel && ( - + {headerLabels && headerLabels.length > 0 && ( + + {headerLabels.map((label, index) => ( + + ))} + )}
{info && info.length > 0 ? ( diff --git a/src/components/InfoViewer/index.ts b/src/components/InfoViewer/index.ts index a4521d495f..afe3aa83da 100644 --- a/src/components/InfoViewer/index.ts +++ b/src/components/InfoViewer/index.ts @@ -2,4 +2,4 @@ import {InfoViewer} from './InfoViewer'; export {InfoViewer}; export * from './utils'; -export type {InfoViewerItem} from './InfoViewer'; +export type {InfoViewerItem, InfoViewerHeaderLabel} from './InfoViewer'; diff --git a/src/components/PDiskPopup/PDiskPopup.tsx b/src/components/PDiskPopup/PDiskPopup.tsx index 6804323a3c..4c5bf835b4 100644 --- a/src/components/PDiskPopup/PDiskPopup.tsx +++ b/src/components/PDiskPopup/PDiskPopup.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import {Flex} from '@gravity-ui/uikit'; +import {Flex, Label} from '@gravity-ui/uikit'; import {selectNodesMap} from '../../store/reducers/nodesList'; import {EFlag} from '../../types/api/enums'; @@ -8,17 +8,24 @@ import {valueIsDefined} from '../../utils'; import {cn} from '../../utils/cn'; import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; import {createPDiskDeveloperUILink} from '../../utils/developerUI/developerUI'; +import {getStateSeverity} from '../../utils/disks/calculatePDiskSeverity'; +import { + NUMERIC_SEVERITY_LABEL_ICON, + NUMERIC_SEVERITY_LABEL_THEME, +} 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 type {InfoViewerHeaderLabel, InfoViewerItem} from '../InfoViewer'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; import {pDiskInfoKeyset} from '../PDiskInfo/i18n'; import {PDiskPageLink} from '../PDiskPageLink/PDiskPageLink'; +import {pDiskPopupKeyset} from './i18n'; + import './PDiskPopup.scss'; const b = cn('pdisk-storage-popup'); @@ -30,41 +37,41 @@ export const preparePDiskData = ( nodeData?: {Host?: string; DC?: string}, withDeveloperUILink?: boolean, ) => { - const {AvailableSize, TotalSize, State, PDiskId, NodeId, Path, Realtime, Type, Device} = data; + const {AvailableSize, TotalSize, PDiskId, NodeId, Path, Realtime, Type, Device} = data; const pdiskData: InfoViewerItem[] = [ - {label: 'State', value: State || 'not available'}, - {label: 'Type', value: Type || 'unknown'}, + {label: pDiskPopupKeyset('label_type'), value: Type || pDiskPopupKeyset('value_unknown')}, ]; if (NodeId) { - pdiskData.push({label: 'Node Id', value: NodeId}); + pdiskData.push({label: pDiskPopupKeyset('label_node-id'), value: NodeId}); } if (nodeData?.Host) { - pdiskData.push({label: 'Host', value: nodeData.Host}); + pdiskData.push({label: pDiskPopupKeyset('label_host'), value: nodeData.Host}); } + if (nodeData?.DC) { - pdiskData.push({label: 'DC', value: nodeData.DC}); + pdiskData.push({label: pDiskPopupKeyset('label_dc'), value: }); } if (Path) { - pdiskData.push({label: 'Path', value: Path}); + pdiskData.push({label: pDiskPopupKeyset('label_path'), value: Path}); } if (isNumeric(TotalSize)) { pdiskData.push({ - label: 'Available', - value: `${bytesToGB(AvailableSize)} of ${bytesToGB(TotalSize)}`, + label: pDiskPopupKeyset('label_available'), + value: `${bytesToGB(AvailableSize)} ${pDiskPopupKeyset('value_of')} ${bytesToGB(TotalSize)}`, }); } if (Realtime && errorColors.includes(Realtime)) { - pdiskData.push({label: 'Realtime', value: Realtime}); + pdiskData.push({label: pDiskPopupKeyset('label_realtime'), value: Realtime}); } if (Device && errorColors.includes(Device)) { - pdiskData.push({label: 'Device', value: Device}); + pdiskData.push({label: pDiskPopupKeyset('label_device'), value: Device}); } if (withDeveloperUILink && valueIsDefined(NodeId) && valueIsDefined(PDiskId)) { @@ -90,6 +97,31 @@ export const preparePDiskData = ( return pdiskData; }; +const preparePDiskHeaderLabels = (data: PreparedPDisk): InfoViewerHeaderLabel[] => { + const labels: InfoViewerHeaderLabel[] = []; + const {State} = data; + + if (!State) { + labels.push({ + value: pDiskPopupKeyset('context_not-available'), + }); + + return labels; + } + + const severity = getStateSeverity(State); + const theme = severity !== undefined ? NUMERIC_SEVERITY_LABEL_THEME[severity] : undefined; + const icon = severity !== undefined ? NUMERIC_SEVERITY_LABEL_ICON[severity] : undefined; + + labels.push({ + value: State, + theme, + icon, + }); + + return labels; +}; + interface PDiskPopupProps { data: PreparedPDisk; } @@ -99,11 +131,17 @@ export const PDiskPopup = ({data}: PDiskPopupProps) => { 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 headerLabels = React.useMemo( + () => preparePDiskHeaderLabels(data), + [data], + ); + const pdiskId = data.StringifiedId; return ( @@ -112,6 +150,7 @@ export const PDiskPopup = ({data}: PDiskPopupProps) => { titleSuffix={pdiskId ?? EMPTY_DATA_PLACEHOLDER} info={info} size="s" + headerLabels={headerLabels} /> ); }; diff --git a/src/components/PDiskPopup/i18n/en.json b/src/components/PDiskPopup/i18n/en.json new file mode 100644 index 0000000000..36bd0da0f7 --- /dev/null +++ b/src/components/PDiskPopup/i18n/en.json @@ -0,0 +1,14 @@ +{ + "context_not-available": "not available", + "label_vdisk": "PDisk", + "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/VDisk/VDisk.tsx b/src/components/VDisk/VDisk.tsx index e49f3c6a22..ddc738a37e 100644 --- a/src/components/VDisk/VDisk.tsx +++ b/src/components/VDisk/VDisk.tsx @@ -1,10 +1,11 @@ -import {BucketPaint} from '@gravity-ui/icons'; import {Icon} from '@gravity-ui/uikit'; import {useStorageQueryParams} from '../../containers/Storage/useStorageQueryParams'; import {useVDiskPagePath} from '../../routes'; import {STORAGE_TYPES} from '../../store/reducers/storage/constants'; +import {EVDiskState} from '../../types/api/vdisk'; import {cn} from '../../utils/cn'; +import {getVDiskStatusIcon} from '../../utils/disks/helpers'; import type {PreparedVDisk} from '../../utils/disks/types'; import {DiskStateProgressBar} from '../DiskStateProgressBar/DiskStateProgressBar'; import {HoverPopup} from '../HoverPopup/HoverPopup'; @@ -44,12 +45,13 @@ export const VDisk = ({ const getVDiskLink = useVDiskPagePath(); const vDiskPath = getVDiskLink({nodeId: data.NodeId, vDiskId: data.StringifiedId}); - // Donor and replicating Vdisks have similar data.replicated and data.VDiskState params - const isNotReplicating = data.Replicated === false && data.VDiskState === 'OK'; - // The difference is only in data.donorMode - const isDonor = data.DonorMode; + const severity = data.Severity; + const isDonor = data.VDiskState === EVDiskState.OK && data.DonorMode; + const isReplicating = + data.Replicated === false && data.VDiskState === EVDiskState.OK && !data.DonorMode; - const isDonorIconShow = isGroupView && isDonor; + const statusIcon = getVDiskStatusIcon(severity); + const showIcon = statusIcon && isGroupView; return ( - +
) : null } diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 937c910c20..6bfea9b22b 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -1,7 +1,5 @@ import React from 'react'; -import {ArrowsRotateLeft, BucketPaint} from '@gravity-ui/icons'; -import type {IconData, LabelProps} from '@gravity-ui/uikit'; import {Flex} from '@gravity-ui/uikit'; import {useVDiskPagePath} from '../../routes'; @@ -13,6 +11,13 @@ 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 { + DISK_COLOR_STATE_TO_NUMERIC_SEVERITY, + NUMERIC_SEVERITY_LABEL_ICON, + NUMERIC_SEVERITY_LABEL_THEME, + VDISK_LABEL_CONFIG, + VDISK_STATE_SEVERITY, +} from '../../utils/disks/constants'; import {isFullVDiskData} from '../../utils/disks/helpers'; import type {PreparedVDisk, UnavailableDonor} from '../../utils/disks/types'; import {useTypedSelector} from '../../utils/hooks'; @@ -22,7 +27,7 @@ import { useIsViewerUser, } from '../../utils/hooks/useIsUserAllowedToMakeChanges'; import {bytesToGB, bytesToSpeed} from '../../utils/utils'; -import type {InfoViewerItem} from '../InfoViewer'; +import type {InfoViewerHeaderLabel, InfoViewerItem} from '../InfoViewer'; import {InfoViewer} from '../InfoViewer'; import {InternalLink} from '../InternalLink'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; @@ -38,9 +43,7 @@ const b = cn('vdisk-storage-popup'); const prepareUnavailableVDiskData = (data: UnavailableDonor, withDeveloperUILink?: boolean) => { const {NodeId, PDiskId, VSlotId, StoragePoolName} = data; - const vdiskData: InfoViewerItem[] = [ - {label: vDiskPopupKeyset('label_state'), value: vDiskPopupKeyset('context_not-available')}, - ]; + const vdiskData: InfoViewerItem[] = []; if (StoragePoolName) { vdiskData.push({label: vDiskPopupKeyset('label_storage-pool'), value: StoragePoolName}); @@ -108,12 +111,7 @@ const prepareVDiskData = ( Recipient, } = data; - const vdiskData: InfoViewerItem[] = [ - { - label: vDiskPopupKeyset('label_state'), - value: VDiskState ?? vDiskPopupKeyset('context_not-available'), - }, - ]; + const vdiskData: InfoViewerItem[] = []; if (StoragePoolName) { vdiskData.push({label: vDiskPopupKeyset('label_storage-pool'), value: StoragePoolName}); @@ -299,6 +297,61 @@ const prepareVDiskData = ( return vdiskData; }; +const prepareHeaderLabels = (data: PreparedVDisk): InfoViewerHeaderLabel[] => { + const labels: InfoViewerHeaderLabel[] = []; + + const {VDiskState, DonorMode, Replicated} = data; + + if (!VDiskState) { + labels.push({ + value: vDiskPopupKeyset('context_not-available'), + }); + + return labels; + } + + const isReplica = Replicated === false && VDiskState === EVDiskState.OK; + const hasReplicationRole = Boolean(DonorMode || isReplica); + + if (DonorMode) { + const config = VDISK_LABEL_CONFIG.donor; + + labels.push({ + value: vDiskPopupKeyset('label_donor'), + theme: config.theme, + icon: config.icon, + }); + } else if (isReplica) { + const config = VDISK_LABEL_CONFIG.replica; + + labels.push({ + value: vDiskPopupKeyset('label_replication'), + theme: config.theme, + icon: config.icon, + }); + } + + const stateSeverity = VDISK_STATE_SEVERITY[VDiskState]; + const isStateOk = stateSeverity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green; + + if (hasReplicationRole && isStateOk) { + return labels; + } + + const stateTheme = + stateSeverity !== undefined ? NUMERIC_SEVERITY_LABEL_THEME[stateSeverity] : undefined; + const stateIcon = + stateSeverity !== undefined ? NUMERIC_SEVERITY_LABEL_ICON[stateSeverity] : undefined; + + labels.push({ + value: VDiskState, + theme: stateTheme, + icon: stateIcon, + }); + + return labels; +}; + interface VDiskPopupProps { data: PreparedVDisk | UnavailableDonor; } @@ -330,29 +383,12 @@ export const VDiskPopup = ({data}: VDiskPopupProps) => { [data, nodeData, isFullData, isUserAllowedToMakeChanges], ); - const vdiskId = isFullVDiskData(data) ? data.StringifiedId : undefined; - - const labelConfig: { - showLabel: boolean; - labelText?: string; - labelIcon?: IconData; - labelTheme?: LabelProps['theme']; - } = { - showLabel: false, - }; - - if (data.DonorMode) { - labelConfig.showLabel = true; - labelConfig.labelText = vDiskPopupKeyset('label_donor'); - labelConfig.labelIcon = BucketPaint; - labelConfig.labelTheme = 'unknown'; - } else if (isFullVDiskData(data) && !data.Replicated) { - labelConfig.showLabel = true; - labelConfig.labelText = vDiskPopupKeyset('label_replication'); - labelConfig.labelIcon = ArrowsRotateLeft; - labelConfig.labelTheme = 'info'; - } + const vdiskId = isFullData ? data.StringifiedId : undefined; + const headerLabels: InfoViewerHeaderLabel[] = React.useMemo( + () => (isFullData ? prepareHeaderLabels(data) : []), + [data], + ); return (
{ titleSuffix={vdiskId ?? EMPTY_DATA_PLACEHOLDER} info={vdiskInfo} size="s" - showLabel={labelConfig.showLabel} - labelText={labelConfig.labelText} - labelTheme={labelConfig.labelTheme} - labelIcon={labelConfig.labelIcon} + headerLabels={headerLabels} /> {pdiskInfo && isViewerUser && }
diff --git a/src/components/VDiskPopup/i18n/en.json b/src/components/VDiskPopup/i18n/en.json index 0c8cf3ae80..1d6958d296 100644 --- a/src/components/VDiskPopup/i18n/en.json +++ b/src/components/VDiskPopup/i18n/en.json @@ -1,6 +1,5 @@ { "context_not-available": "not available", - "label_state": "State", "label_storage-pool": "StoragePool", "label_donor": "Donor", "label_node-id": "NodeId", diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss index c1bbbe723e..2140a04113 100644 --- a/src/styles/mixins.scss +++ b/src/styles/mixins.scss @@ -280,11 +280,15 @@ --entity-state-background-color: var(--g-color-base-danger-light); --entity-state-fill-color: var(--g-color-base-danger-medium); } - &_grey { + &_darkgrey { --entity-state-font-color: var(--g-color-text-secondary); --entity-state-border-color: var(--g-color-line-generic-hover); --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/types/api/enums.ts b/src/types/api/enums.ts index 5eddd10905..9e5917a006 100644 --- a/src/types/api/enums.ts +++ b/src/types/api/enums.ts @@ -8,4 +8,5 @@ export enum EFlag { Yellow = 'Yellow', Orange = 'Orange', Red = 'Red', + DarkGrey = 'DarkGrey', } diff --git a/src/utils/disks/__test__/calculateVDiskSeverity.test.ts b/src/utils/disks/__test__/calculateVDiskSeverity.test.ts index 33a887d1d8..d499bbc208 100644 --- a/src/utils/disks/__test__/calculateVDiskSeverity.test.ts +++ b/src/utils/disks/__test__/calculateVDiskSeverity.test.ts @@ -156,7 +156,7 @@ describe('VDisk state', () => { DonorMode: true, }); - expect(severity1).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue); + expect(severity1).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.DarkGrey); expect(severity2).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow); expect(severity3).toEqual(DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red); }); 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 cf6f6deda1..a78fabea29 100644 --- a/src/utils/disks/calculateVDiskSeverity.ts +++ b/src/utils/disks/calculateVDiskSeverity.ts @@ -19,7 +19,7 @@ export function calculateVDiskSeverity< const {DiskSpace, VDiskState, FrontQueues, Replicated, DonorMode} = vDisk; // if the disk is not available, this determines its status severity regardless of other features - if (!VDiskState || DonorMode) { + if (!VDiskState) { return NOT_AVAILABLE_SEVERITY; } @@ -32,9 +32,13 @@ export function calculateVDiskSeverity< let severity = Math.max(DiskSpaceSeverity, VDiskSpaceSeverity, FrontQueuesSeverity); - // donors are always in the not replicated state since they are leftovers - if (Replicated === false && severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green) { - severity = DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue; + const isHealthy = severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green; + + // If VDisk is healthy and not replicated, adjust color based on its role + if (isHealthy && Replicated === false) { + severity = DonorMode + ? DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.DarkGrey // donor + : DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue; // replicating } return severity; @@ -53,8 +57,8 @@ function getColorSeverity(color?: EFlag) { return NOT_AVAILABLE_SEVERITY; } - // Blue is reserved for not replicated VDisks - if (color === EFlag.Blue) { + // Blue is reserved for not replicated VDisks. DarkGrey is reserved for donors. + if (color === EFlag.Blue || color === EFlag.DarkGrey) { return DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green; } diff --git a/src/utils/disks/constants.ts b/src/utils/disks/constants.ts index fcbd53e320..0ee081e42b 100644 --- a/src/utils/disks/constants.ts +++ b/src/utils/disks/constants.ts @@ -1,3 +1,12 @@ +import { + ArrowsRotateLeft, + BucketPaint, + CircleCheck, + CircleExclamation, + 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'; @@ -10,6 +19,7 @@ export const DISK_COLOR_STATE_TO_NUMERIC_SEVERITY: Record = { Yellow: 3, Orange: 4, Red: 5, + DarkGrey: 6, } as const; type SeverityToColor = Record; @@ -52,3 +62,33 @@ export const PDISK_STATE_SEVERITY = { [TPDiskState.DeviceIoError]: DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red, [TPDiskState.Stopped]: DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red, }; + +export const NUMERIC_SEVERITY_LABEL_THEME: Record = { + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green]: 'success', + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue]: 'info', + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow]: 'warning', + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Orange]: 'warning', + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red]: 'danger', + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Grey]: 'unknown', + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.DarkGrey]: 'unknown', +}; + +export const NUMERIC_SEVERITY_LABEL_ICON: Record = { + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Green]: CircleCheck, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Blue]: ArrowsRotateLeft, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow]: TriangleExclamation, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Orange]: TriangleExclamation, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red]: CircleExclamation, + [DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.DarkGrey]: BucketPaint, +}; + +export const VDISK_LABEL_CONFIG: Record = { + donor: { + theme: 'unknown', + icon: BucketPaint, + }, + replica: { + theme: 'info', + icon: ArrowsRotateLeft, + }, +}; diff --git a/src/utils/disks/helpers.ts b/src/utils/disks/helpers.ts index da39644c45..bf94a3f92d 100644 --- a/src/utils/disks/helpers.ts +++ b/src/utils/disks/helpers.ts @@ -1,3 +1,5 @@ +import type {IconData} from '@gravity-ui/uikit'; + import {valueIsDefined} from '..'; import type {VDiskBlobIndexStatParams} from '../../store/reducers/vdisk/vdisk'; import {EFlag} from '../../types/api/enums'; @@ -8,6 +10,7 @@ import { DISK_COLOR_STATE_TO_NUMERIC_SEVERITY, DISK_NUMERIC_SEVERITY_TO_STATE_COLOR, NOT_AVAILABLE_SEVERITY_COLOR, + NUMERIC_SEVERITY_LABEL_ICON, } from './constants'; import type {PreparedVDisk} from './types'; @@ -55,3 +58,19 @@ export function getVDiskId(params: VDiskBlobIndexStatParams) { : [params.nodeId, params.pDiskId, params.vDiskSlotId]; return parts.join('-'); } + +export function getVDiskStatusIcon(severity?: number): IconData | undefined { + if (severity === undefined) { + return undefined; + } + + // Display icon only for error and donor + if ( + severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Red || + severity === DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.DarkGrey + ) { + return NUMERIC_SEVERITY_LABEL_ICON[severity]; + } + + return undefined; +} From e2c8a26443b8688f0531f9c1ded6448ca12aeb6c Mon Sep 17 00:00:00 2001 From: Daria Vorontsova Date: Thu, 20 Nov 2025 17:16:54 +0300 Subject: [PATCH 04/16] Fix bugs --- .../EntityStatusNew/EntityStatus.tsx | 4 ++++ src/components/EntityStatusNew/utils.ts | 3 +++ src/components/PDiskPopup/PDiskPopup.tsx | 2 +- src/components/StatusIconNew/StatusIcon.tsx | 1 + src/components/VDiskPopup/VDiskPopup.tsx | 23 +++++++++++++++---- .../Cluster/ClusterInfo/utils/utils.tsx | 1 + 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/EntityStatusNew/EntityStatus.tsx b/src/components/EntityStatusNew/EntityStatus.tsx index 041de2c848..502da88407 100644 --- a/src/components/EntityStatusNew/EntityStatus.tsx +++ b/src/components/EntityStatusNew/EntityStatus.tsx @@ -21,6 +21,7 @@ const EFlagToLabelTheme: Record = { [EFlag.Grey]: 'unknown', [EFlag.Orange]: 'orange', [EFlag.Yellow]: 'warning', + [EFlag.DarkGrey]: 'unknown', }; const EFlagToStatusName: Record = { @@ -39,6 +40,9 @@ const EFlagToStatusName: Record = { get [EFlag.Grey]() { return i18n('title_grey'); }, + get [EFlag.DarkGrey]() { + return i18n('title_grey'); + }, get [EFlag.Blue]() { return i18n('title_blue'); }, diff --git a/src/components/EntityStatusNew/utils.ts b/src/components/EntityStatusNew/utils.ts index 16a896fdfd..b64959ef72 100644 --- a/src/components/EntityStatusNew/utils.ts +++ b/src/components/EntityStatusNew/utils.ts @@ -18,6 +18,9 @@ export const EFlagToDescription: Record = { get [EFlag.Grey]() { return i18n('context_grey'); }, + get [EFlag.DarkGrey]() { + return i18n('context_grey'); + }, get [EFlag.Blue]() { return i18n('context_blue'); }, diff --git a/src/components/PDiskPopup/PDiskPopup.tsx b/src/components/PDiskPopup/PDiskPopup.tsx index 4c5bf835b4..d48b1dc5a3 100644 --- a/src/components/PDiskPopup/PDiskPopup.tsx +++ b/src/components/PDiskPopup/PDiskPopup.tsx @@ -97,7 +97,7 @@ export const preparePDiskData = ( return pdiskData; }; -const preparePDiskHeaderLabels = (data: PreparedPDisk): InfoViewerHeaderLabel[] => { +export const preparePDiskHeaderLabels = (data: PreparedPDisk): InfoViewerHeaderLabel[] => { const labels: InfoViewerHeaderLabel[] = []; const {State} = data; diff --git a/src/components/StatusIconNew/StatusIcon.tsx b/src/components/StatusIconNew/StatusIcon.tsx index 7808c7edc1..8631846af0 100644 --- a/src/components/StatusIconNew/StatusIcon.tsx +++ b/src/components/StatusIconNew/StatusIcon.tsx @@ -17,6 +17,7 @@ const EFlagToIcon: Record) => React [EFlag.Red]: CircleExclamation, [EFlag.Green]: CircleCheck, [EFlag.Grey]: PlugConnection, + [EFlag.DarkGrey]: PlugConnection, }; interface StatusIconProps extends Omit { diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 6bfea9b22b..fac4e5ba17 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -31,7 +31,7 @@ import type {InfoViewerHeaderLabel, InfoViewerItem} from '../InfoViewer'; import {InfoViewer} from '../InfoViewer'; import {InternalLink} from '../InternalLink'; import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; -import {preparePDiskData} from '../PDiskPopup/PDiskPopup'; +import {preparePDiskData, preparePDiskHeaderLabels} from '../PDiskPopup/PDiskPopup'; import {vDiskInfoKeyset} from '../VDiskInfo/i18n'; import {vDiskPopupKeyset} from './i18n'; @@ -382,12 +382,17 @@ export const VDiskPopup = ({data}: VDiskPopupProps) => { preparePDiskData(data.PDisk, nodeData, isUserAllowedToMakeChanges), [data, nodeData, isFullData, isUserAllowedToMakeChanges], ); + const pdiskHeaderLabels = React.useMemo( + () => (isFullData && data.PDisk ? preparePDiskHeaderLabels(data.PDisk) : []), + [data, isFullData], + ); const vdiskId = isFullData ? data.StringifiedId : undefined; + const pdiskId = isFullData ? data.PDisk?.StringifiedId : undefined; - const headerLabels: InfoViewerHeaderLabel[] = React.useMemo( + const vdiskHeaderLabels: InfoViewerHeaderLabel[] = React.useMemo( () => (isFullData ? prepareHeaderLabels(data) : []), - [data], + [data, isFullData], ); return (
@@ -396,9 +401,17 @@ export const VDiskPopup = ({data}: VDiskPopupProps) => { titleSuffix={vdiskId ?? EMPTY_DATA_PLACEHOLDER} info={vdiskInfo} size="s" - headerLabels={headerLabels} + headerLabels={vdiskHeaderLabels} /> - {pdiskInfo && isViewerUser && } + {pdiskInfo && isViewerUser && ( + + )}
); }; diff --git a/src/containers/Cluster/ClusterInfo/utils/utils.tsx b/src/containers/Cluster/ClusterInfo/utils/utils.tsx index 3d71dbc107..fe90659d7e 100644 --- a/src/containers/Cluster/ClusterInfo/utils/utils.tsx +++ b/src/containers/Cluster/ClusterInfo/utils/utils.tsx @@ -22,6 +22,7 @@ const COLORS_PRIORITY: Record = { Orange: 2, Red: 1, Grey: 0, + DarkGrey: -1, }; const getDCInfo = (cluster: TClusterInfo) => { From fd5210aa6ab8e96e7175c3b51b6b91bea420bf18 Mon Sep 17 00:00:00 2001 From: Daria Vorontsova Date: Mon, 24 Nov 2025 10:14:32 +0300 Subject: [PATCH 05/16] Fix bugs --- .../DiskStateProgressBar.scss | 9 +- .../DiskStateProgressBar.tsx | 13 +- .../EntityStatusNew/EntityStatus.scss | 5 - .../EntityStatusNew/EntityStatus.tsx | 54 +-- src/components/EntityStatusNew/i18n/index.ts | 7 - src/components/EntityStatusNew/utils.ts | 27 -- .../HealthcheckStatus/HealthcheckStatus.tsx | 59 +-- src/components/HealthcheckStatus/i18n/en.json | 7 - src/components/InfoViewer/InfoViewer.scss | 8 +- src/components/InfoViewer/InfoViewer.tsx | 58 +-- src/components/InfoViewer/index.ts | 2 +- src/components/PDiskPopup/PDiskPopup.scss | 5 - src/components/PDiskPopup/PDiskPopup.tsx | 125 +++--- src/components/StatusIconNew/StatusIcon.tsx | 1 - src/components/VDisk/VDisk.tsx | 14 +- src/components/VDiskPopup/VDiskPopup.scss | 8 +- src/components/VDiskPopup/VDiskPopup.tsx | 373 ++++++++++-------- .../YDBDefinitionList/YDBDefinitionList.scss | 26 ++ .../YDBDefinitionList/YDBDefinitionList.tsx | 85 +++- src/containers/Cluster/Cluster.tsx | 4 +- .../Cluster/ClusterInfo/utils/utils.tsx | 1 - src/styles/index.scss | 1 + src/styles/labels.scss | 4 + src/types/api/enums.ts | 1 - .../__test__/calculateVDiskSeverity.test.ts | 2 +- src/utils/disks/calculateVDiskSeverity.ts | 16 +- src/utils/disks/constants.ts | 49 +-- src/utils/disks/helpers.ts | 29 +- src/utils/healthStatus/common.ts | 52 +++ src/utils/healthStatus/healthCheck.ts | 69 ++++ .../healthStatus}/i18n/en.json | 7 + .../healthStatus}/i18n/index.ts | 2 +- src/utils/healthStatus/selfCheck.ts | 43 ++ 33 files changed, 652 insertions(+), 514 deletions(-) delete mode 100644 src/components/EntityStatusNew/i18n/index.ts delete mode 100644 src/components/EntityStatusNew/utils.ts delete mode 100644 src/components/HealthcheckStatus/i18n/en.json delete mode 100644 src/components/PDiskPopup/PDiskPopup.scss create mode 100644 src/styles/labels.scss create mode 100644 src/utils/healthStatus/common.ts create mode 100644 src/utils/healthStatus/healthCheck.ts rename src/{components/EntityStatusNew => utils/healthStatus}/i18n/en.json (71%) rename src/{components/HealthcheckStatus => utils/healthStatus}/i18n/index.ts (70%) create mode 100644 src/utils/healthStatus/selfCheck.ts diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss index 5a6d29795c..22aa17910b 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; @@ -83,9 +86,9 @@ background-image: repeating-linear-gradient( 135deg, transparent 0, - transparent 4px, - var(--entity-state-fill-color) 4px, - var(--entity-state-fill-color) 8px + transparent var(--stripe-width), + var(--entity-state-fill-color) var(--stripe-width), + var(--entity-state-fill-color) var(--stripe-step) ); } } diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx index b9d42a853a..cbb0d18dc7 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {SETTING_KEYS} from '../../store/reducers/settings/constants'; import {cn} from '../../utils/cn'; +import {DONOR_COLOR} from '../../utils/disks/constants'; import {getSeverityColor} from '../../utils/disks/helpers'; import {useSetting} from '../../utils/hooks'; @@ -19,6 +20,7 @@ interface DiskStateProgressBarProps { striped?: boolean; content?: React.ReactNode; className?: string; + isDonor?: boolean; } export function DiskStateProgressBar({ @@ -31,6 +33,7 @@ export function DiskStateProgressBar({ content, striped, className, + isDonor, }: DiskStateProgressBarProps) { const [inverted] = useSetting(SETTING_KEYS.INVERTED_DISKS); @@ -43,9 +46,13 @@ export function DiskStateProgressBar({ striped, }; - const color = severity !== undefined && getSeverityColor(severity); - if (color) { - mods[color.toLowerCase()] = true; + if (isDonor) { + mods[DONOR_COLOR.toLocaleLowerCase()] = true; + } else { + const color = severity !== undefined && getSeverityColor(severity); + if (color) { + mods[color.toLocaleLowerCase()] = true; + } } const renderAllocatedPercent = () => { diff --git a/src/components/EntityStatusNew/EntityStatus.scss b/src/components/EntityStatusNew/EntityStatus.scss index 524d3353ba..11c8039580 100644 --- a/src/components/EntityStatusNew/EntityStatus.scss +++ b/src/components/EntityStatusNew/EntityStatus.scss @@ -2,9 +2,4 @@ &__note { color: inherit; } - - &_orange.g-label { - color: var(--g-color-private-orange-500); - background-color: var(--g-color-private-orange-100); - } } diff --git a/src/components/EntityStatusNew/EntityStatus.tsx b/src/components/EntityStatusNew/EntityStatus.tsx index 502da88407..995857c8bc 100644 --- a/src/components/EntityStatusNew/EntityStatus.tsx +++ b/src/components/EntityStatusNew/EntityStatus.tsx @@ -3,51 +3,15 @@ import React from 'react'; import type {LabelProps} from '@gravity-ui/uikit'; import {ActionTooltip, Flex, HelpMark, Label} from '@gravity-ui/uikit'; -import {EFlag} from '../../types/api/enums'; +import type {EFlag} from '../../types/api/enums'; import {cn} from '../../utils/cn'; +import {getEFlagView} from '../../utils/healthStatus/healthCheck'; import {StatusIcon} from '../StatusIconNew/StatusIcon'; -import i18n from './i18n'; -import {EFlagToDescription} from './utils'; - import './EntityStatus.scss'; const b = cn('ydb-entity-status-new'); -const EFlagToLabelTheme: Record = { - [EFlag.Red]: 'danger', - [EFlag.Blue]: 'info', - [EFlag.Green]: 'success', - [EFlag.Grey]: 'unknown', - [EFlag.Orange]: 'orange', - [EFlag.Yellow]: 'warning', - [EFlag.DarkGrey]: 'unknown', -}; - -const EFlagToStatusName: Record = { - get [EFlag.Red]() { - return i18n('title_red'); - }, - get [EFlag.Yellow]() { - return i18n('title_yellow'); - }, - get [EFlag.Orange]() { - return i18n('title_orange'); - }, - get [EFlag.Green]() { - return i18n('title_green'); - }, - get [EFlag.Grey]() { - return i18n('title_grey'); - }, - get [EFlag.DarkGrey]() { - return i18n('title_grey'); - }, - get [EFlag.Blue]() { - return i18n('title_blue'); - }, -}; - interface EntityStatusLabelProps { status: EFlag; note?: React.ReactNode; @@ -65,18 +29,14 @@ function EntityStatusLabel({ size = 'm', iconSize = 14, }: EntityStatusLabelProps) { - const theme = EFlagToLabelTheme[status]; + const {theme, title, description} = getEFlagView(status); + return ( - -