diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss index e8ceb7643..9a56776f8 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss @@ -4,10 +4,12 @@ $block: &; $border-width: 1px; - $outer-border-radius: 4px; + $outer-border-radius: var(--g-border-radius-s); $inner-border-radius: $outer-border-radius - $border-width; + $outer-compact-border-radius: var(--g-border-radius-xs); + $inner-compact-border-radius: $outer-compact-border-radius - $border-width; - --progress-bar-full-height: var(--g-text-body-3-line-height); + --progress-bar-full-height: var(--g-text-subheader-2-line-height); --progress-bar-compact-height: 12px; --stripe-width: 4px; @@ -16,6 +18,8 @@ position: relative; z-index: 0; + overflow: hidden; + min-width: 50px; height: var(--progress-bar-full-height); @@ -25,13 +29,14 @@ border: $border-width solid var(--entity-state-border-color); border-radius: $outer-border-radius; background-color: var(--entity-state-background-color); - @include mixins.entity-state-colors(); + + @include mixins.entity-state-colors($block); &_compact { min-width: 0; height: var(--progress-bar-compact-height); - border-radius: 2px; + border-radius: $outer-compact-border-radius; } &_faded { @@ -42,6 +47,10 @@ opacity: 0.5; } + &_darkened { + opacity: 0.7; + } + &_empty { color: var(--g-color-text-hint); border-style: dashed; @@ -78,7 +87,7 @@ } &_compact { - border-radius: 1px; + border-radius: $inner-compact-border-radius; } &_inverted { @@ -93,9 +102,10 @@ position: relative; z-index: 2; - margin-right: var(--g-spacing-1); + margin-right: var(--g-spacing-half); - font-size: var(--g-text-body-1-font-size); + font-family: var(--g-text-caption-font-family); + font-size: var(--g-text-caption-1-font-size); // bar height minus borders line-height: calc(var(--progress-bar-full-height) - #{$border-width * 2}); @@ -106,7 +116,7 @@ position: relative; z-index: 2; - margin-left: var(--g-spacing-1); + margin-left: calc(var(--g-spacing-1) - $border-width); color: var(--entity-state-border-color); } diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx index ac93c6ab8..6a6e81b8f 100644 --- a/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx +++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.tsx @@ -24,6 +24,8 @@ interface DiskStateProgressBarProps { className?: string; isDonor?: boolean; withIcon?: boolean; + highlighted?: boolean; + darkened?: boolean; } export function DiskStateProgressBar({ @@ -38,6 +40,8 @@ export function DiskStateProgressBar({ className, isDonor, withIcon, + highlighted, + darkened, }: DiskStateProgressBarProps) { const [inverted] = useSetting(SETTING_KEYS.INVERTED_DISKS); @@ -48,6 +52,8 @@ export function DiskStateProgressBar({ empty, inactive, striped, + highlighted, + darkened, }; if (isDonor) { diff --git a/src/components/HoverPopup/HoverPopup.tsx b/src/components/HoverPopup/HoverPopup.tsx index ac902e9a5..30e6b64ee 100644 --- a/src/components/HoverPopup/HoverPopup.tsx +++ b/src/components/HoverPopup/HoverPopup.tsx @@ -34,44 +34,50 @@ export const HoverPopup = ({ delayOpen = DEBOUNCE_TIMEOUT, }: HoverPopupProps) => { const [isPopupVisible, setIsPopupVisible] = React.useState(false); + const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false); + const [isFocused, setIsFocused] = React.useState(false); + const anchor = React.useRef(null); const debouncedHandleShowPopup = React.useMemo( () => debounce(() => { setIsPopupVisible(true); - onShowPopup?.(); }, delayOpen), - [onShowPopup, delayOpen], + [delayOpen], ); const hidePopup = React.useCallback(() => { setIsPopupVisible(false); - onHidePopup?.(); - }, [onHidePopup]); + }, []); const debouncedHandleHidePopup = React.useMemo( - () => debounce(hidePopup, delayClose), + () => + debounce(() => { + hidePopup(); + }, delayClose), [hidePopup, delayClose], ); - const onMouseEnter = debouncedHandleShowPopup; + const onMouseEnter = () => { + debouncedHandleHidePopup.cancel(); + debouncedHandleShowPopup(); + }; const onMouseLeave = () => { debouncedHandleShowPopup.cancel(); debouncedHandleHidePopup(); }; - const [isPopupContentHovered, setIsPopupContentHovered] = React.useState(false); - const [isFocused, setIsFocused] = React.useState(false); - const onPopupMouseEnter = React.useCallback(() => { setIsPopupContentHovered(true); - }, []); + debouncedHandleHidePopup.cancel(); + }, [debouncedHandleHidePopup]); const onPopupMouseLeave = React.useCallback(() => { setIsPopupContentHovered(false); - }, []); + debouncedHandleHidePopup(); + }, [debouncedHandleHidePopup]); const onPopupContextMenu = React.useCallback(() => { setIsFocused(true); @@ -87,7 +93,26 @@ export const HoverPopup = ({ hidePopup(); }, [hidePopup]); - const open = isPopupVisible || showPopup || isPopupContentHovered || isFocused; + const internalOpen = isPopupVisible || isPopupContentHovered || isFocused; + const open = internalOpen || showPopup; + + const prevInternalOpenRef = React.useRef(internalOpen); + + React.useEffect(() => { + const prev = prevInternalOpenRef.current; + + if (prev === internalOpen) { + return; + } + + if (internalOpen) { + onShowPopup?.(); + } else { + onHidePopup?.(); + } + + prevInternalOpenRef.current = internalOpen; + }, [internalOpen, onShowPopup, onHidePopup]); return ( diff --git a/src/components/VDisk/VDisk.tsx b/src/components/VDisk/VDisk.tsx index 76eb71ce2..ddcd7ac8c 100644 --- a/src/components/VDisk/VDisk.tsx +++ b/src/components/VDisk/VDisk.tsx @@ -22,6 +22,8 @@ export interface VDiskProps { delayOpen?: number; delayClose?: number; withIcon?: boolean; + highlighted?: boolean; + darkened?: boolean; } export const VDisk = ({ @@ -35,13 +37,15 @@ export const VDisk = ({ delayClose, delayOpen, withIcon, + highlighted, + darkened, }: 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; + const isDonor = data.DonorMode; return ( diff --git a/src/components/VDisk/VDiskWithDonorsStack.tsx b/src/components/VDisk/VDiskWithDonorsStack.tsx index e1411310d..ee2374a06 100644 --- a/src/components/VDisk/VDiskWithDonorsStack.tsx +++ b/src/components/VDisk/VDiskWithDonorsStack.tsx @@ -10,6 +10,9 @@ interface VDiskWithDonorsStackProps extends VDiskProps { data?: PreparedVDisk; className?: string; stackClassName?: string; + highlightedVDisk?: string; + setHighlightedVDisk?: (id?: string) => void; + progressBarClassName?: string; } export function VDiskWithDonorsStack({ @@ -17,30 +20,54 @@ export function VDiskWithDonorsStack({ className, stackClassName, withIcon, + highlightedVDisk, + setHighlightedVDisk, ...restProps }: VDiskWithDonorsStackProps) { const {Donors: donors, ...restData} = data || {}; + const stackId = data?.StringifiedId; + const isHighlighted = Boolean(stackId && highlightedVDisk === stackId); + const isDarkened = Boolean(highlightedVDisk && highlightedVDisk !== stackId); + + const handleShowPopup = () => { + if (stackId) { + setHighlightedVDisk?.(stackId); + } + }; + + const handleHidePopup = () => { + setHighlightedVDisk?.(undefined); + }; + + const commonVDiskProps: Partial = { + withIcon, + showPopup: isHighlighted, + highlighted: isHighlighted, + darkened: isDarkened, + onShowPopup: handleShowPopup, + onHidePopup: handleHidePopup, + ...restProps, + }; + const content = donors && donors.length > 0 ? ( - + {donors.map((donor) => { const isFullData = isFullVDiskData(donor); - // donor and acceptor are always in the same group return ( ); })} ) : ( - + ); return
{content}
; diff --git a/src/components/VDiskPopup/VDiskPopup.tsx b/src/components/VDiskPopup/VDiskPopup.tsx index 315c5df0b..0cb3dd91e 100644 --- a/src/components/VDiskPopup/VDiskPopup.tsx +++ b/src/components/VDiskPopup/VDiskPopup.tsx @@ -345,21 +345,15 @@ const prepareHeaderLabels = (data: PreparedVDisk): YDBDefinitionListHeaderLabel[ theme: donorConfig.theme, icon: donorConfig.icon, }); - } - - if (isReplicatingColor) { - if (!DonorMode) { - const replicaConfig = VDISK_LABEL_CONFIG.replica; + } else if (isReplicatingColor) { + const replicaConfig = VDISK_LABEL_CONFIG.replica; - labels.push({ - id: 'replication', - value: vDiskPopupKeyset('label_replication'), - theme: replicaConfig.theme, - icon: replicaConfig.icon, - }); - } - - return labels; + labels.push({ + id: 'replication', + value: vDiskPopupKeyset('label_replication'), + theme: replicaConfig.theme, + icon: replicaConfig.icon, + }); } const severity = VDiskState ? getStateSeverity(VDiskState) : NOT_AVAILABLE_SEVERITY; diff --git a/src/components/YDBDefinitionList/YDBDefinitionList.scss b/src/components/YDBDefinitionList/YDBDefinitionList.scss index b54f57ba8..067b9e868 100644 --- a/src/components/YDBDefinitionList/YDBDefinitionList.scss +++ b/src/components/YDBDefinitionList/YDBDefinitionList.scss @@ -12,10 +12,6 @@ color: var(--g-color-text-secondary); } - &__properties-list { - max-width: calc(100% - 40px); - } - &__footer { margin-top: var(--g-spacing-3); } diff --git a/src/containers/Storage/Disks/Disks.scss b/src/containers/Storage/Disks/Disks.scss index 4bb544e9d..165ad7605 100644 --- a/src/containers/Storage/Disks/Disks.scss +++ b/src/containers/Storage/Disks/Disks.scss @@ -2,7 +2,7 @@ display: flex; flex-direction: row; align-items: center; - gap: 20px; + gap: var(--g-spacing-4); width: max-content; @@ -19,17 +19,17 @@ flex-shrink: 0; } &__vdisk-progress-bar { - --progress-bar-compact-height: 18px; + --progress-bar-compact-height: 20px; - border-radius: 4px; + border-radius: var(--g-border-radius-m); } &__pdisk-item { - min-width: 80px; - margin-right: 4px; + min-width: 55px; + margin-right: var(--g-spacing-1); &_with-dc-margin { - margin-right: 12px; + margin-right: var(--g-spacing-3); } &:last-child { @@ -39,6 +39,6 @@ &__pdisk-progress-bar { --progress-bar-full-height: 20px; - text-align: left; + border-radius: var(--g-border-radius-m); } } diff --git a/src/containers/Storage/Disks/Disks.tsx b/src/containers/Storage/Disks/Disks.tsx index 66c2243c0..138bc929c 100644 --- a/src/containers/Storage/Disks/Disks.tsx +++ b/src/containers/Storage/Disks/Disks.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import {Flex, useLayoutContext} from '@gravity-ui/uikit'; import {VDisk} from '../../../components/VDisk/VDisk'; @@ -16,18 +14,25 @@ import './Disks.scss'; const b = cn('ydb-storage-disks'); -const VDISKS_CONTAINER_WIDTH = 300; +const VDISKS_CONTAINER_WIDTH = 316; interface DisksProps { vDisks?: PreparedVDisk[]; viewContext?: StorageViewContext; erasure?: Erasure; withIcon?: boolean; + highlightedVDisk?: string; + setHighlightedVDisk?: (id?: string) => void; } -export function Disks({vDisks = [], viewContext, erasure, withIcon}: DisksProps) { - const [highlightedVDisk, setHighlightedVDisk] = React.useState(); - +export function Disks({ + vDisks = [], + viewContext, + erasure, + withIcon, + highlightedVDisk, + setHighlightedVDisk, +}: DisksProps) { const vDisksWithDCMargins = useVDisksWithDCMargins(vDisks, erasure); const { @@ -76,8 +81,8 @@ export function Disks({vDisks = [], viewContext, erasure, withIcon}: DisksProps) interface DisksItemProps { vDisk: PreparedVDisk; inactive?: boolean; - highlightedVDisk: string | undefined; - setHighlightedVDisk: (id: string | undefined) => void; + highlightedVDisk?: string; + setHighlightedVDisk?: (id?: string) => void; unavailableVDiskWidth?: number; withDCMargin?: boolean; withIcon?: boolean; @@ -100,6 +105,9 @@ function VDiskItem({ const minWidth = isNumeric(vDiskToShow.AllocatedSize) ? undefined : unavailableVDiskWidth; const flexGrow = Number(vDiskToShow.AllocatedSize) || 1; + const isHighlighted = highlightedVDisk === vDiskId; + const darkened = Boolean(highlightedVDisk && highlightedVDisk !== vDiskId); + return (
setHighlightedVDisk(vDiskId)} - onHidePopup={() => setHighlightedVDisk(undefined)} + onShowPopup={() => setHighlightedVDisk?.(vDiskId)} + onHidePopup={() => setHighlightedVDisk?.(undefined)} progressBarClassName={b('vdisk-progress-bar')} + highlighted={isHighlighted} + darkened={darkened} />
); @@ -127,6 +137,9 @@ function PDiskItem({ }: DisksItemProps) { const vDiskId = vDisk.StringifiedId; + const isHighlighted = highlightedVDisk === vDiskId; + const darkened = Boolean(highlightedVDisk && highlightedVDisk !== vDiskId); + if (!vDisk.PDisk) { return null; } @@ -136,12 +149,14 @@ function PDiskItem({ className={b('pdisk-item', {['with-dc-margin']: withDCMargin})} progressBarClassName={b('pdisk-progress-bar')} data={vDisk.PDisk} - showPopup={highlightedVDisk === vDiskId} + showPopup={isHighlighted} delayOpen={DISKS_POPUP_DEBOUNCE_TIMEOUT} delayClose={DISKS_POPUP_DEBOUNCE_TIMEOUT} - onShowPopup={() => setHighlightedVDisk(vDiskId)} - onHidePopup={() => setHighlightedVDisk(undefined)} + onShowPopup={() => setHighlightedVDisk?.(vDiskId)} + onHidePopup={() => setHighlightedVDisk?.(undefined)} withIcon={withIcon} + highlighted={isHighlighted} + darkened={darkened} /> ); } diff --git a/src/containers/Storage/PDisk/PDisk.tsx b/src/containers/Storage/PDisk/PDisk.tsx index b79e72095..631cbfa65 100644 --- a/src/containers/Storage/PDisk/PDisk.tsx +++ b/src/containers/Storage/PDisk/PDisk.tsx @@ -31,6 +31,10 @@ interface PDiskProps { delayOpen?: number; delayClose?: number; withIcon?: boolean; + highlighted?: boolean; + darkened?: boolean; + highlightedPDisk?: string; + setHighlightedPDisk?: (id?: string) => void; } export const PDisk = ({ @@ -46,6 +50,10 @@ export const PDisk = ({ delayOpen = DISKS_POPUP_DEBOUNCE_TIMEOUT, delayClose = DISKS_POPUP_DEBOUNCE_TIMEOUT, withIcon, + highlighted, + darkened, + highlightedPDisk, + setHighlightedPDisk, }: PDiskProps) => { const {NodeId, PDiskId} = data; const pDiskIdsDefined = !isNil(NodeId) && !isNil(PDiskId); @@ -58,26 +66,37 @@ export const PDisk = ({ return (
- {vDisks.map((vdisk) => ( -
- -
- ))} + {vDisks.map((vdisk) => { + const vDiskId = vdisk.StringifiedId; + const vDiskHighlighted = highlightedPDisk === vDiskId; + const vDiskDarkened = Boolean(highlightedPDisk && highlightedPDisk !== vDiskId); + + return ( +
+ setHighlightedPDisk?.(vDiskId)} + onHidePopup={() => setHighlightedPDisk?.(undefined)} + highlighted={vDiskHighlighted} + darkened={vDiskDarkened} + /> +
+ ); + })}
); }; @@ -107,6 +126,8 @@ export const PDisk = ({ diskAllocatedPercent={data.AllocatedPercent} severity={data.Severity} className={progressBarClassName} + highlighted={highlighted} + darkened={darkened} />
diff --git a/src/containers/Storage/PaginatedStorageGroupsTable/columns/StorageGroupsColumns.scss b/src/containers/Storage/PaginatedStorageGroupsTable/columns/StorageGroupsColumns.scss index b4c4a9a1a..05b0f1d68 100644 --- a/src/containers/Storage/PaginatedStorageGroupsTable/columns/StorageGroupsColumns.scss +++ b/src/containers/Storage/PaginatedStorageGroupsTable/columns/StorageGroupsColumns.scss @@ -20,3 +20,6 @@ font-weight: 500; } } + +@include mixins.hover-dim-column-class('ydb-storage-groups-columns__disks-column'); +@include mixins.hover-dim-column-class('ydb-storage-groups-columns__vdisks-column'); diff --git a/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx b/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx index 1a4c932c5..5378860d9 100644 --- a/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx +++ b/src/containers/Storage/PaginatedStorageGroupsTable/columns/columns.tsx @@ -16,7 +16,7 @@ import {formatToMs} from '../../../../utils/timeParsers'; import {bytesToGB, bytesToSpeed} from '../../../../utils/utils'; import {Disks} from '../../Disks/Disks'; import {VDisks} from '../../VDisks/VDisks'; -import {getDegradedSeverity} from '../../utils'; +import {getDegradedSeverity, isTopLevelStorageContext} from '../../utils'; import i18n from '../i18n'; import { @@ -123,10 +123,10 @@ const usageColumn: StorageGroupsColumn = { width: 85, resizeMinWidth: 75, render: ({row}) => { - return !isNil(row.Usage) ? ( - - ) : ( + return isNil(row.Usage) ? ( EMPTY_DATA_PLACEHOLDER + ) : ( + ); }, align: DataTable.LEFT, @@ -137,13 +137,13 @@ const diskSpaceUsageColumn: StorageGroupsColumn = { width: 115, resizeMinWidth: 75, render: ({row}) => { - return !isNil(row.DiskSpaceUsage) ? ( + return isNil(row.DiskSpaceUsage) ? ( + EMPTY_DATA_PLACEHOLDER + ) : ( - ) : ( - EMPTY_DATA_PLACEHOLDER ); }, align: DataTable.LEFT, @@ -215,9 +215,9 @@ const latencyColumn: StorageGroupsColumn = { header: STORAGE_GROUPS_COLUMNS_TITLES.Latency, width: 100, render: ({row}) => { - return !isNil(row.LatencyPutTabletLogMs) - ? formatToMs(row.LatencyPutTabletLogMs) - : EMPTY_DATA_PLACEHOLDER; + return isNil(row.LatencyPutTabletLogMs) + ? EMPTY_DATA_PLACEHOLDER + : formatToMs(row.LatencyPutTabletLogMs); }, align: DataTable.RIGHT, }; @@ -227,50 +227,64 @@ const allocationUnitsColumn: StorageGroupsColumn = { header: STORAGE_GROUPS_COLUMNS_TITLES.AllocationUnits, width: 150, render: ({row}) => { - return !isNil(row.AllocationUnits) - ? formatNumber(row.AllocationUnits) - : EMPTY_DATA_PLACEHOLDER; + return isNil(row.AllocationUnits) + ? EMPTY_DATA_PLACEHOLDER + : formatNumber(row.AllocationUnits); }, align: DataTable.RIGHT, }; -const getVDisksColumn = (data?: GetStorageColumnsData): StorageGroupsColumn => ({ - name: STORAGE_GROUPS_COLUMNS_IDS.VDisks, - 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 - resizeable: false, - sortable: false, -}); +const getVDisksColumn = (data?: GetStorageColumnsData): StorageGroupsColumn => { + const highlightEnabled = isTopLevelStorageContext(data?.viewContext); + const highlightedVDisk = highlightEnabled ? data?.highlightedVDisksVDisk : undefined; + const setHighlightedVDisk = highlightEnabled ? data?.setHighlightedVDisksVDisk : undefined; -const getDisksColumn = (data?: GetStorageColumnsData): StorageGroupsColumn => ({ - name: STORAGE_GROUPS_COLUMNS_IDS.VDisksPDisks, - header: STORAGE_GROUPS_COLUMNS_TITLES.VDisksPDisks, - className: b('disks-column'), - render: ({row}) => { - return ( + return { + name: STORAGE_GROUPS_COLUMNS_IDS.VDisks, + header: STORAGE_GROUPS_COLUMNS_TITLES.VDisks, + className: b('vdisks-column', {highlighted: highlightEnabled}), + render: ({row}) => ( + + ), + align: DataTable.CENTER, + width: 475, // usually 8-9 vdisks, this width corresponds to 8 vdisks, column is expanded if more + resizeable: false, + sortable: false, + }; +}; + +const getDisksColumn = (data?: GetStorageColumnsData): StorageGroupsColumn => { + const highlightEnabled = isTopLevelStorageContext(data?.viewContext); + const highlightedVDisk = highlightEnabled ? data?.highlightedVDisk : undefined; + const setHighlightedVDisk = highlightEnabled ? data?.setHighlightedVDisk : undefined; + + return { + name: STORAGE_GROUPS_COLUMNS_IDS.VDisksPDisks, + header: STORAGE_GROUPS_COLUMNS_TITLES.VDisksPDisks, + className: b('disks-column', {highlighted: highlightEnabled}), + render: ({row}) => ( - ); - }, - align: DataTable.CENTER, - width: 900, - resizeable: false, - sortable: false, -}); + ), + align: DataTable.CENTER, + width: 800, + resizeable: false, + sortable: false, + }; +}; export const getStorageTopGroupsColumns: StorageColumnsGetter = () => { return [groupIdColumn, typeColumn, erasureColumn, usageColumn, usedColumn, limitColumn]; diff --git a/src/containers/Storage/PaginatedStorageGroupsTable/columns/hooks.ts b/src/containers/Storage/PaginatedStorageGroupsTable/columns/hooks.ts index eaffdf271..c1f72f350 100644 --- a/src/containers/Storage/PaginatedStorageGroupsTable/columns/hooks.ts +++ b/src/containers/Storage/PaginatedStorageGroupsTable/columns/hooks.ts @@ -28,8 +28,20 @@ export function useStorageGroupsSelectedColumns({ const isViewerUser = useIsViewerUser(); const bridgeModeEnabled = useBridgeModeEnabled(); + const [highlightedVDisk, setHighlightedVDisk] = React.useState(); + + const [highlightedVDisksVDisk, setHighlightedVDisksVDisk] = React.useState< + string | undefined + >(); + const columns = React.useMemo(() => { - const allColumns = getStorageGroupsColumns({viewContext}); + const allColumns = getStorageGroupsColumns({ + viewContext, + highlightedVDisk, + setHighlightedVDisk, + highlightedVDisksVDisk, + setHighlightedVDisksVDisk, + }); const filteredByBridge = bridgeModeEnabled ? allColumns : allColumns.filter((c) => c.name !== STORAGE_GROUPS_COLUMNS_IDS.PileName); @@ -44,7 +56,14 @@ export function useStorageGroupsSelectedColumns({ return filteredColumns; } return filteredColumns.filter((column) => !isViewerGroupsColumn(column.name)); - }, [isUserAllowedToMakeChanges, viewContext, isViewerUser, bridgeModeEnabled]); + }, [ + isUserAllowedToMakeChanges, + viewContext, + isViewerUser, + bridgeModeEnabled, + highlightedVDisk, + highlightedVDisksVDisk, + ]); const requiredColumns = React.useMemo(() => { if (visibleEntities === VISIBLE_ENTITIES.missing) { diff --git a/src/containers/Storage/PaginatedStorageGroupsTable/columns/types.ts b/src/containers/Storage/PaginatedStorageGroupsTable/columns/types.ts index a7d30a47a..3c8225d21 100644 --- a/src/containers/Storage/PaginatedStorageGroupsTable/columns/types.ts +++ b/src/containers/Storage/PaginatedStorageGroupsTable/columns/types.ts @@ -6,6 +6,12 @@ export type StorageGroupsColumn = Column; export interface GetStorageColumnsData { viewContext?: StorageViewContext; + + highlightedVDisk?: string; + setHighlightedVDisk?: (id: string | undefined) => void; + + highlightedVDisksVDisk?: string; + setHighlightedVDisksVDisk?: (id: string | undefined) => void; } export interface GetStorageGroupsColumnsParams { diff --git a/src/containers/Storage/PaginatedStorageNodesTable/columns/StorageNodesColumns.scss b/src/containers/Storage/PaginatedStorageNodesTable/columns/StorageNodesColumns.scss index 16e429a71..7c7424f64 100644 --- a/src/containers/Storage/PaginatedStorageNodesTable/columns/StorageNodesColumns.scss +++ b/src/containers/Storage/PaginatedStorageNodesTable/columns/StorageNodesColumns.scss @@ -1,4 +1,4 @@ -@import '../../../../styles/mixins.scss'; +@use '../../../../styles/mixins.scss'; .ydb-storage-nodes-columns { &__pdisks-column { @@ -17,3 +17,5 @@ flex-shrink: 0; } } + +@include mixins.hover-dim-column-class('ydb-storage-nodes-columns__pdisks-column'); diff --git a/src/containers/Storage/PaginatedStorageNodesTable/columns/columns.tsx b/src/containers/Storage/PaginatedStorageNodesTable/columns/columns.tsx index ad06d6294..0b73e683d 100644 --- a/src/containers/Storage/PaginatedStorageNodesTable/columns/columns.tsx +++ b/src/containers/Storage/PaginatedStorageNodesTable/columns/columns.tsx @@ -25,6 +25,7 @@ import { import type {NodesColumn} from '../../../../components/nodesColumns/types'; import {cn} from '../../../../utils/cn'; import {PDisk} from '../../PDisk/PDisk'; +import {isTopLevelStorageContext} from '../../utils'; import type {GetStorageNodesColumnsParams} from './types'; @@ -35,11 +36,17 @@ const b = cn('ydb-storage-nodes-columns'); export const getPDisksColumn = ({ viewContext, columnsSettings, + highlightedPDisk, + setHighlightedPDisk, }: GetStorageNodesColumnsParams): NodesColumn => { + const highlightEnabled = isTopLevelStorageContext(viewContext); + const coloredPDisk = highlightEnabled ? highlightedPDisk : undefined; + const setColoredPDisk = highlightEnabled ? setHighlightedPDisk : undefined; + return { name: NODES_COLUMNS_IDS.PDisks, header: NODES_COLUMNS_TITLES.PDisks, - className: b('pdisks-column'), + className: b('pdisks-column', {highlighted: highlightEnabled}), width: columnsSettings?.pDiskContainerWidth, render: ({row}) => { return ( @@ -49,6 +56,11 @@ export const getPDisksColumn = ({ (vdisk) => vdisk.PDiskId === pDisk.PDiskId, ); + const id = `${row.NodeId}-${pDisk.PDiskId}`; + + const highlighted = highlightedPDisk === id; + const darkened = Boolean(highlightedPDisk && highlightedPDisk !== id); + return (
setColoredPDisk?.(id)} + onHidePopup={() => setColoredPDisk?.(undefined)} + highlighted={highlighted} + darkened={darkened} + highlightedPDisk={coloredPDisk} + setHighlightedPDisk={setColoredPDisk} />
); @@ -73,6 +92,8 @@ export const getStorageNodesColumns = ({ database, viewContext, columnsSettings, + highlightedPDisk, + setHighlightedPDisk, }: GetStorageNodesColumnsParams): NodesColumn[] => { const columns: NodesColumn[] = [ getNodeIdColumn(), @@ -89,7 +110,7 @@ export const getStorageNodesColumns = ({ getDiskSpaceUsageColumn(), getVersionColumn(), getMissingDisksColumn(), - getPDisksColumn({viewContext, columnsSettings}), + getPDisksColumn({viewContext, columnsSettings, highlightedPDisk, setHighlightedPDisk}), getTabletsColumn({database}), ]; diff --git a/src/containers/Storage/PaginatedStorageNodesTable/columns/hooks.ts b/src/containers/Storage/PaginatedStorageNodesTable/columns/hooks.ts index cba6c1b78..bddaf4e5d 100644 --- a/src/containers/Storage/PaginatedStorageNodesTable/columns/hooks.ts +++ b/src/containers/Storage/PaginatedStorageNodesTable/columns/hooks.ts @@ -24,14 +24,18 @@ export function useStorageNodesSelectedColumns({ }: GetStorageNodesColumnsParams) { const bridgeModeEnabled = useBridgeModeEnabled(); + const [highlightedPDisk, setHighlightedPDisk] = React.useState(); + const columns = React.useMemo(() => { const all = getStorageNodesColumns({ database, viewContext, columnsSettings, + highlightedPDisk, + setHighlightedPDisk, }); return bridgeModeEnabled ? all : all.filter((c) => c.name !== NODES_COLUMNS_IDS.PileName); - }, [database, viewContext, columnsSettings, bridgeModeEnabled]); + }, [database, viewContext, columnsSettings, bridgeModeEnabled, highlightedPDisk]); const requiredColumns = React.useMemo(() => { if (visibleEntities === VISIBLE_ENTITIES.missing) { diff --git a/src/containers/Storage/PaginatedStorageNodesTable/columns/types.ts b/src/containers/Storage/PaginatedStorageNodesTable/columns/types.ts index 4f559144a..e65f3f054 100644 --- a/src/containers/Storage/PaginatedStorageNodesTable/columns/types.ts +++ b/src/containers/Storage/PaginatedStorageNodesTable/columns/types.ts @@ -11,4 +11,7 @@ export interface GetStorageNodesColumnsParams { database?: string; viewContext?: StorageViewContext; columnsSettings?: StorageNodesColumnsSettings; + + highlightedPDisk?: string; + setHighlightedPDisk?: (id: string | undefined) => void; } diff --git a/src/containers/Storage/VDisks/VDisks.scss b/src/containers/Storage/VDisks/VDisks.scss index a8927d4af..5b9a10224 100644 --- a/src/containers/Storage/VDisks/VDisks.scss +++ b/src/containers/Storage/VDisks/VDisks.scss @@ -4,8 +4,8 @@ } &__item { - width: 90px; - margin-right: 6px; + width: 55px; + margin-right: var(--g-spacing-1); &_with-dc-margin { margin-right: 12px; @@ -21,4 +21,8 @@ } } } + + &__vdisks-progress-bar { + border-radius: var(--g-border-radius-m); + } } diff --git a/src/containers/Storage/VDisks/VDisks.tsx b/src/containers/Storage/VDisks/VDisks.tsx index b268cbd59..296c7d61d 100644 --- a/src/containers/Storage/VDisks/VDisks.tsx +++ b/src/containers/Storage/VDisks/VDisks.tsx @@ -15,9 +15,18 @@ interface VDisksProps { viewContext?: StorageViewContext; erasure?: Erasure; withIcon?: boolean; + highlightedVDisk?: string; + setHighlightedVDisk?: (id: string | undefined) => void; } -export function VDisks({vDisks, viewContext, erasure, withIcon}: VDisksProps) { +export function VDisks({ + vDisks, + viewContext, + erasure, + withIcon, + highlightedVDisk, + setHighlightedVDisk, +}: VDisksProps) { const vDisksWithDCMargins = useVDisksWithDCMargins(vDisks, erasure); return ( @@ -33,6 +42,9 @@ export function VDisks({vDisks, viewContext, erasure, withIcon}: VDisksProps) { className={b('item', { 'with-dc-margin': vDisksWithDCMargins.includes(index), })} + highlightedVDisk={highlightedVDisk} + setHighlightedVDisk={setHighlightedVDisk} + progressBarClassName={b('vdisks-progress-bar')} /> ))} diff --git a/src/containers/Storage/utils/index.ts b/src/containers/Storage/utils/index.ts index 8d7b7fea7..a929b8cf0 100644 --- a/src/containers/Storage/utils/index.ts +++ b/src/containers/Storage/utils/index.ts @@ -1,9 +1,10 @@ import React from 'react'; +import {isNil} from 'lodash'; + import {selectNodesMap} from '../../../store/reducers/nodesList'; import type {PreparedStorageGroup} from '../../../store/reducers/storage/types'; import type {Erasure} from '../../../types/api/storage'; -import {valueIsDefined} from '../../../utils'; import type {PreparedVDisk} from '../../../utils/disks/types'; import {generateEvaluator} from '../../../utils/generateEvaluator'; import {useTypedSelector} from '../../../utils/hooks'; @@ -30,25 +31,30 @@ export const getDegradedSeverity = (group: PreparedStorageGroup) => { export function isVdiskActive(vDisk: PreparedVDisk, viewContext?: StorageViewContext) { let isActive = true; - if (valueIsDefined(vDisk.VDiskId?.GroupID) && viewContext?.groupId) { + if (!isNil(vDisk.VDiskId?.GroupID) && viewContext?.groupId) { isActive &&= String(vDisk.VDiskId.GroupID) === viewContext.groupId; } - if (valueIsDefined(vDisk.NodeId) && viewContext?.nodeId) { + if (!isNil(vDisk.NodeId) && viewContext?.nodeId) { isActive &&= String(vDisk.NodeId) === viewContext.nodeId; } - if (valueIsDefined(vDisk.PDiskId) && viewContext?.pDiskId) { + if (!isNil(vDisk.PDiskId) && viewContext?.pDiskId) { isActive &&= String(vDisk.PDiskId) === viewContext.pDiskId; } - if (valueIsDefined(vDisk.VDiskSlotId) && viewContext?.vDiskSlotId) { + if (!isNil(vDisk.VDiskSlotId) && viewContext?.vDiskSlotId) { isActive &&= String(vDisk.VDiskSlotId) === viewContext.vDiskSlotId; } return isActive; } +export function isTopLevelStorageContext(context?: StorageViewContext): boolean { + // highlight the disk only where we are not committed to a specific node / p-disk / v-disk slot + return isNil(context?.nodeId) && isNil(context?.pDiskId) && isNil(context?.vDiskSlotId); +} + const DEFAULT_ENTITIES_COUNT = 10; // NodePage - 1 node @@ -58,11 +64,7 @@ const DEFAULT_ENTITIES_COUNT = 10; export function getStorageNodesInitialEntitiesCount( context?: StorageViewContext, ): number | undefined { - if ( - valueIsDefined(context?.nodeId) || - valueIsDefined(context?.pDiskId) || - valueIsDefined(context?.vDiskSlotId) - ) { + if (!isNil(context?.nodeId) || !isNil(context?.pDiskId) || !isNil(context?.vDiskSlotId)) { return 1; } @@ -76,10 +78,10 @@ export function getStorageNodesInitialEntitiesCount( export function getStorageGroupsInitialEntitiesCount( context?: StorageViewContext, ): number | undefined { - if (valueIsDefined(context?.groupId)) { + if (!isNil(context?.groupId)) { return 1; } - if (valueIsDefined(context?.vDiskSlotId)) { + if (!isNil(context?.vDiskSlotId)) { return 1; } diff --git a/src/containers/Storage/utils/useStorageColumnsSettings.ts b/src/containers/Storage/utils/useStorageColumnsSettings.ts index d166fee07..b3049f254 100644 --- a/src/containers/Storage/utils/useStorageColumnsSettings.ts +++ b/src/containers/Storage/utils/useStorageColumnsSettings.ts @@ -5,7 +5,7 @@ import type {StorageNodesPaginatedTableData} from '../types'; const PDISK_VDISK_WIDTH = 3; const PDISK_GAP_WIDTH = 2; -const PDISK_MIN_WIDTH = 120; +const PDISK_MIN_WIDTH = 165; const PDISK_MARGIN = 10; const MAX_SLOTS_DEFAULT = 1; const PAGNATED_TABLE_CELL_HORIZONTAL_PADDING = 10; diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss index 2d1f18a82..569b02cf6 100644 --- a/src/styles/mixins.scss +++ b/src/styles/mixins.scss @@ -240,7 +240,7 @@ } } -@mixin entity-state-colors { +@mixin entity-state-colors($block: null) { --entity-state-border-color: var(--g-color-base-misc-heavy); --entity-state-background-color: var(--g-color-base-misc-light); --entity-state-fill-color: var(--g-color-base-misc-medium); @@ -248,24 +248,43 @@ &_green { --entity-state-font-color: var(--g-color-text-positive); - --entity-state-border-color: var(--g-color-base-positive-heavy); + --entity-state-border-color: var(--g-color-base-positive-medium); --entity-state-background-color: var(--g-color-base-positive-light); --entity-state-fill-color: var(--g-color-base-positive-medium); + + &#{$block}_highlighted { + --entity-state-border-color: var(--g-color-base-positive-medium-hover); + --entity-state-background-color: var(--g-color-base-positive-light-hover); + --entity-state-fill-color: var(--g-color-base-positive-heavy); + } } &_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-shadow-color: var(--g-color-base-info-medium); --entity-state-background-color: var(--g-color-base-info-light); --entity-state-fill-color: var(--g-color-base-info-medium); + + &#{$block}_highlighted { + --entity-state-border-color: var(--g-color-base-info-heavy-hover); + --entity-state-shadow-color: var(--g-color-base-info-heavy); + --entity-state-background-color: var(--g-color-base-info-light-hover); + --entity-state-fill-color: var(--g-color-base-info-medium-hover); + } } &_yellow { --entity-state-font-color: var(--g-color-text-warning); - --entity-state-border-color: var(--g-color-base-warning-heavy); - --entity-state-background-color: var(--g-color-base-yellow-light); - --entity-state-fill-color: var(--g-color-base-yellow-medium); + --entity-state-border-color: var(--g-color-base-warning-medium); + --entity-state-background-color: var(--g-color-base-warning-light); + --entity-state-fill-color: var(--g-color-base-warning-medium); + + &#{$block}_highlighted { + --entity-state-border-color: var(--g-color-base-warning-medium-hover); + --entity-state-background-color: var(--g-color-base-warning-light-hover); + --entity-state-fill-color: var(--g-color-base-warning-medium-hover); + } } &_red { @@ -273,6 +292,12 @@ --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); + + &#{$block}_highlighted { + --entity-state-border-color: var(--g-color-base-danger-heavy-hover); + --entity-state-background-color: var(--g-color-base-danger-light); + --entity-state-fill-color: var(--g-color-base-danger-medium-hover); + } } &_darkgrey { @@ -281,10 +306,29 @@ --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; + + &#{$block}_highlighted { + --entity-state-border-color: var(--g-color-base-neutral-heavy-hover); + --entity-state-shadow-color: var(--g-color-base-neutral-light-hover); + --entity-state-fill-color: var(--g-color-base-neutral-light-hover); + --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); + --entity-state-background-color: var(--g-color-base-neutral-light-hover); + + &#{$block}_highlighted { + --entity-state-background-color: var(--g-color-base-neutral-medium); + } + } +} + +@mixin hover-dim-column-class($column-class) { + .ydb-paginated-table__table:has(.#{$column-class}:hover) + .#{$column-class}_highlighted + .storage-disk-progress-bar:not(.storage-disk-progress-bar_highlighted) { + opacity: 0.7; } } diff --git a/src/utils/disks/helpers.ts b/src/utils/disks/helpers.ts index d38e4c7f5..4e8c57cec 100644 --- a/src/utils/disks/helpers.ts +++ b/src/utils/disks/helpers.ts @@ -66,10 +66,9 @@ export function getVDiskStatusIcon(severity?: number, isDonor?: boolean): IconDa } 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) { + if (isDonor) { return DONOR_ICON; }