From 7bb1d67913a2814ace42f8fbc01de5220c3bbb89 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Wed, 13 Mar 2024 17:08:14 +0300 Subject: [PATCH 1/2] feat: add PDisk page --- src/components/EntityStatus/EntityStatus.scss | 56 +------- src/components/EntityStatus/EntityStatus.tsx | 31 +--- .../ExternalLinkWithIcon.tsx | 24 ---- src/components/InfoViewer/InfoViewer.tsx | 4 +- .../InfoViewerSkeleton.scss} | 10 +- .../InfoViewerSkeleton.tsx} | 12 +- .../LinkWithIcon.scss} | 2 +- src/components/LinkWithIcon/LinkWithIcon.tsx | 39 +++++ src/components/PDiskInfo/PDiskInfo.scss | 8 ++ src/components/PDiskInfo/PDiskInfo.tsx | 124 ++++++++++++++++ src/components/PDiskInfo/i18n/en.json | 15 ++ src/components/PDiskInfo/i18n/index.ts | 7 + src/components/PageMeta/PageMeta.scss | 10 ++ src/components/PageMeta/PageMeta.tsx | 17 +++ src/components/StatusIcon/StatusIcon.scss | 69 +++++++++ src/components/StatusIcon/StatusIcon.tsx | 42 ++++++ src/containers/App/Content.tsx | 7 + src/containers/App/appSlots.tsx | 6 + .../Cluster/ClusterInfo/ClusterInfo.scss | 4 + .../Cluster/ClusterInfo/ClusterInfo.tsx | 8 +- src/containers/Header/Header.tsx | 7 +- src/containers/Header/breadcrumbs.tsx | 39 ++++- src/containers/Header/i18n/en.json | 7 + src/containers/Header/i18n/index.ts | 7 + .../Node/NodeStructure/NodeStructure.tsx | 4 - src/containers/Node/NodeStructure/Pdisk.tsx | 107 +------------- src/containers/Node/NodeStructure/Vdisk.tsx | 3 +- src/containers/PDisk/PDisk.scss | 41 ++++++ src/containers/PDisk/PDisk.tsx | 133 ++++++++++++++++++ src/containers/PDisk/PDiskGroups.tsx | 49 +++++++ src/containers/PDisk/i18n/en.json | 6 + src/containers/PDisk/i18n/index.ts | 7 + src/containers/PDisk/shared.ts | 3 + .../DiskStateProgressBar.tsx | 12 +- .../StorageGroups/getStorageGroupsColumns.tsx | 13 ++ .../Info/ExternalTable/ExternalTable.tsx | 4 +- src/routes.ts | 12 +- src/services/api.ts | 2 +- src/store/reducers/header/types.ts | 6 +- src/store/reducers/index.ts | 2 + src/store/reducers/nodes/types.ts | 1 + src/store/reducers/pdisk/pdisk.ts | 116 +++++++++++++++ src/store/reducers/pdisk/types.ts | 29 ++++ src/store/reducers/pdisk/utils.ts | 54 +++++++ src/store/reducers/storage/types.ts | 2 +- src/store/reducers/storage/utils.ts | 4 +- src/utils/disks/constants.ts | 8 ++ src/utils/disks/helpers.ts | 9 ++ src/utils/index.ts | 4 + 49 files changed, 921 insertions(+), 265 deletions(-) delete mode 100644 src/components/ExternalLinkWithIcon/ExternalLinkWithIcon.tsx rename src/{containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.scss => components/InfoViewerSkeleton/InfoViewerSkeleton.scss} (82%) rename src/{containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.tsx => components/InfoViewerSkeleton/InfoViewerSkeleton.tsx} (62%) rename src/components/{ExternalLinkWithIcon/ExternalLinkWithIcon.scss => LinkWithIcon/LinkWithIcon.scss} (75%) create mode 100644 src/components/LinkWithIcon/LinkWithIcon.tsx create mode 100644 src/components/PDiskInfo/PDiskInfo.scss create mode 100644 src/components/PDiskInfo/PDiskInfo.tsx create mode 100644 src/components/PDiskInfo/i18n/en.json create mode 100644 src/components/PDiskInfo/i18n/index.ts create mode 100644 src/components/PageMeta/PageMeta.scss create mode 100644 src/components/PageMeta/PageMeta.tsx create mode 100644 src/components/StatusIcon/StatusIcon.scss create mode 100644 src/components/StatusIcon/StatusIcon.tsx create mode 100644 src/containers/Header/i18n/en.json create mode 100644 src/containers/Header/i18n/index.ts create mode 100644 src/containers/PDisk/PDisk.scss create mode 100644 src/containers/PDisk/PDisk.tsx create mode 100644 src/containers/PDisk/PDiskGroups.tsx create mode 100644 src/containers/PDisk/i18n/en.json create mode 100644 src/containers/PDisk/i18n/index.ts create mode 100644 src/containers/PDisk/shared.ts create mode 100644 src/store/reducers/pdisk/pdisk.ts create mode 100644 src/store/reducers/pdisk/types.ts create mode 100644 src/store/reducers/pdisk/utils.ts diff --git a/src/components/EntityStatus/EntityStatus.scss b/src/components/EntityStatus/EntityStatus.scss index 51360442f..fd22f6ea1 100644 --- a/src/components/EntityStatus/EntityStatus.scss +++ b/src/components/EntityStatus/EntityStatus.scss @@ -58,61 +58,7 @@ } } - &__status-color, - &__status-icon { - flex-shrink: 0; - - margin-right: 8px; - - border-radius: 3px; - &_size_xs { - aspect-ratio: 1; - - width: 12px; - height: 12px; - } - &_size_s { - aspect-ratio: 1; - - width: 16px; - height: 16px; - } - &_size_m { - aspect-ratio: 1; - - width: 18px; - height: 18px; - } - - &_size_l { - width: 27px; - height: 27px; - } - } - - &__status-color { - &_state_green { - background-color: var(--ydb-color-status-green); - } - &_state_yellow { - background-color: var(--ydb-color-status-yellow); - } - &_state_blue { - background-color: var(--ydb-color-status-blue); - } - &_state_red { - background-color: var(--ydb-color-status-red); - } - &_state_grey { - background-color: var(--ydb-color-status-grey); - } - &_state_orange { - background-color: var(--ydb-color-status-orange); - } - } - - &__label, - &__status-icon { + &__label { &_state_blue { color: var(--ydb-color-status-blue); } diff --git a/src/components/EntityStatus/EntityStatus.tsx b/src/components/EntityStatus/EntityStatus.tsx index 1cdc0d739..dda59ff55 100644 --- a/src/components/EntityStatus/EntityStatus.tsx +++ b/src/components/EntityStatus/EntityStatus.tsx @@ -1,23 +1,13 @@ import {Link} from 'react-router-dom'; import cn from 'bem-cn-lite'; -import {Icon, Link as UIKitLink} from '@gravity-ui/uikit'; +import {Link as UIKitLink} from '@gravity-ui/uikit'; import {EFlag} from '../../types/api/enums'; -import circleExclamationIcon from '../../assets/icons/circle-exclamation.svg'; -import circleInfoIcon from '../../assets/icons/circle-info.svg'; -import circleTimesIcon from '../../assets/icons/circle-xmark.svg'; -import triangleExclamationIcon from '../../assets/icons/triangle-exclamation.svg'; +import {StatusIcon, type StatusIconMode, type StatusIconSize} from '../StatusIcon/StatusIcon'; import {ClipboardButton} from '../ClipboardButton'; import './EntityStatus.scss'; -const icons = { - [EFlag.Blue]: circleInfoIcon, - [EFlag.Yellow]: circleExclamationIcon, - [EFlag.Orange]: triangleExclamationIcon, - [EFlag.Red]: circleTimesIcon, -}; - const b = cn('entity-status'); interface EntityStatusProps { @@ -27,8 +17,8 @@ interface EntityStatusProps { path?: string; iconPath?: string; - size?: 'xs' | 's' | 'm' | 'l'; - mode?: 'color' | 'icons'; + size?: StatusIconSize; + mode?: StatusIconMode; showStatus?: boolean; externalLink?: boolean; @@ -64,18 +54,7 @@ export function EntityStatus({ return null; } - const modifiers = {state: status.toLowerCase(), size}; - - if (mode === 'icons' && status in icons) { - return ( - - ); - } - - return
; + return ; }; const renderStatusLink = () => { return ( diff --git a/src/components/ExternalLinkWithIcon/ExternalLinkWithIcon.tsx b/src/components/ExternalLinkWithIcon/ExternalLinkWithIcon.tsx deleted file mode 100644 index a6fc45c9c..000000000 --- a/src/components/ExternalLinkWithIcon/ExternalLinkWithIcon.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import block from 'bem-cn-lite'; - -import {Link} from '@gravity-ui/uikit'; - -import {Icon} from '../Icon/Icon'; - -import './ExternalLinkWithIcon.scss'; - -const b = block('ydb-external-link-with-icon'); - -interface ExternalLinkWithIconProps { - title: string; - url: string; -} - -export const ExternalLinkWithIcon = ({title, url}: ExternalLinkWithIconProps) => { - return ( - - {title} - {'\u00a0'} - - - ); -}; diff --git a/src/components/InfoViewer/InfoViewer.tsx b/src/components/InfoViewer/InfoViewer.tsx index 6cc13ecca..50f118979 100644 --- a/src/components/InfoViewer/InfoViewer.tsx +++ b/src/components/InfoViewer/InfoViewer.tsx @@ -8,7 +8,7 @@ export interface InfoViewerItem { value: ReactNode; } -interface InfoViewerProps { +export interface InfoViewerProps { title?: string; info?: InfoViewerItem[]; dots?: boolean; @@ -20,7 +20,7 @@ interface InfoViewerProps { const b = cn('info-viewer'); -const InfoViewer = ({ +export const InfoViewer = ({ title, info, dots = true, diff --git a/src/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.scss b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.scss similarity index 82% rename from src/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.scss rename to src/components/InfoViewerSkeleton/InfoViewerSkeleton.scss index 0da666d6e..6c0cad6e9 100644 --- a/src/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.scss +++ b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.scss @@ -1,10 +1,8 @@ -.ydb-cluster-info-skeleton { +.ydb-info-viewer-skeleton { display: flex; flex-direction: column; gap: 16px; - margin-top: 5px; - &__row { display: flex; align-items: flex-start; @@ -39,10 +37,4 @@ min-width: 200px; max-width: 20%; } - - &__versions { - min-width: 400px; - max-width: 40%; - height: 36px; - } } diff --git a/src/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.tsx b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx similarity index 62% rename from src/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.tsx rename to src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx index 3e5c28fdd..c6414f748 100644 --- a/src/containers/Cluster/ClusterInfoSkeleton/ClusterInfoSkeleton.tsx +++ b/src/components/InfoViewerSkeleton/InfoViewerSkeleton.tsx @@ -2,9 +2,9 @@ import block from 'bem-cn-lite'; import {Skeleton} from '@gravity-ui/uikit'; -import './ClusterInfoSkeleton.scss'; +import './InfoViewerSkeleton.scss'; -const b = block('ydb-cluster-info-skeleton'); +const b = block('ydb-info-viewer-skeleton'); const SkeletonLabel = () => (
@@ -13,12 +13,12 @@ const SkeletonLabel = () => (
); -interface ClusterInfoSkeletonProps { +interface InfoViewerSkeletonProps { className?: string; rows?: number; } -export const ClusterInfoSkeleton = ({rows = 8, className}: ClusterInfoSkeletonProps) => ( +export const InfoViewerSkeleton = ({rows = 8, className}: InfoViewerSkeletonProps) => (
{[...new Array(rows)].map((_, index) => (
@@ -26,9 +26,5 @@ export const ClusterInfoSkeleton = ({rows = 8, className}: ClusterInfoSkeletonPr
))} -
- - -
); diff --git a/src/components/ExternalLinkWithIcon/ExternalLinkWithIcon.scss b/src/components/LinkWithIcon/LinkWithIcon.scss similarity index 75% rename from src/components/ExternalLinkWithIcon/ExternalLinkWithIcon.scss rename to src/components/LinkWithIcon/LinkWithIcon.scss index e62dbdd4c..9eea5fa3b 100644 --- a/src/components/ExternalLinkWithIcon/ExternalLinkWithIcon.scss +++ b/src/components/LinkWithIcon/LinkWithIcon.scss @@ -1,4 +1,4 @@ -.ydb-external-link-with-icon { +.ydb-link-with-icon { display: flex; flex-wrap: nowrap; align-items: center; diff --git a/src/components/LinkWithIcon/LinkWithIcon.tsx b/src/components/LinkWithIcon/LinkWithIcon.tsx new file mode 100644 index 000000000..a1f5eb2c0 --- /dev/null +++ b/src/components/LinkWithIcon/LinkWithIcon.tsx @@ -0,0 +1,39 @@ +import block from 'bem-cn-lite'; + +import {Link} from '@gravity-ui/uikit'; + +import {Icon} from '../Icon/Icon'; +import {InternalLink} from '../InternalLink'; +import './LinkWithIcon.scss'; + +const b = block('ydb-link-with-icon'); + +interface ExternalLinkWithIconProps { + title: string; + url: string; + external?: boolean; +} + +export const LinkWithIcon = ({title, url, external = true}: ExternalLinkWithIconProps) => { + const linkContent = ( + <> + {title} + {'\u00a0'} + + + ); + + if (external) { + return ( + + {linkContent} + + ); + } + + return ( + + {linkContent} + + ); +}; diff --git a/src/components/PDiskInfo/PDiskInfo.scss b/src/components/PDiskInfo/PDiskInfo.scss new file mode 100644 index 000000000..0313bf2fd --- /dev/null +++ b/src/components/PDiskInfo/PDiskInfo.scss @@ -0,0 +1,8 @@ +.ydb-pdisk-info { + &__links { + display: flex; + flex-flow: row wrap; + + gap: 12px; + } +} diff --git a/src/components/PDiskInfo/PDiskInfo.tsx b/src/components/PDiskInfo/PDiskInfo.tsx new file mode 100644 index 000000000..3f5875c6e --- /dev/null +++ b/src/components/PDiskInfo/PDiskInfo.tsx @@ -0,0 +1,124 @@ +import type {PreparedPDisk} from '../../utils/disks/types'; +import {createPDiskDeveloperUILink} from '../../utils/developerUI/developerUI'; +import {cn} from '../../utils/cn'; +import {formatStorageValuesToGb} from '../../utils/dataFormatters/dataFormatters'; +import {valueIsDefined} from '../../utils'; +import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants'; +import {getPDiskPagePath} from '../../routes'; + +import type {InfoViewerItem} from '../InfoViewer'; +import {InfoViewer, type InfoViewerProps} from '../InfoViewer/InfoViewer'; +import {EntityStatus} from '../EntityStatus/EntityStatus'; +import {LinkWithIcon} from '../LinkWithIcon/LinkWithIcon'; +import {ProgressViewer} from '../ProgressViewer/ProgressViewer'; +import {pDiskInfoKeyset} from './i18n'; + +import './PDiskInfo.scss'; + +const b = cn('ydb-pdisk-info'); + +interface PDiskInfoProps extends Omit { + pDisk: T; + nodeId?: number | string | null; + isPDiskPage?: boolean; +} + +export function PDiskInfo({ + pDisk, + nodeId, + isPDiskPage = false, + ...infoViewerProps +}: PDiskInfoProps) { + const { + PDiskId, + Path, + Guid, + Category, + Type, + Device, + Realtime, + State, + SerialNumber, + TotalSize, + AvailableSize, + } = pDisk; + + const total = Number(TotalSize); + const available = Number(AvailableSize); + + const pdiskInfo: InfoViewerItem[] = []; + + if (valueIsDefined(Path)) { + pdiskInfo.push({label: pDiskInfoKeyset('path'), value: Path}); + } + if (valueIsDefined(Guid)) { + pdiskInfo.push({label: pDiskInfoKeyset('guid'), value: Guid}); + } + if (valueIsDefined(Category)) { + pdiskInfo.push({label: pDiskInfoKeyset('category'), value: Category}); + pdiskInfo.push({label: pDiskInfoKeyset('type'), value: Type}); + } + if (total >= 0 && available >= 0) { + pdiskInfo.push({ + label: pDiskInfoKeyset('size'), + value: ( + + ), + }); + } + if (valueIsDefined(State)) { + pdiskInfo.push({label: pDiskInfoKeyset('state'), value: State}); + } + if (valueIsDefined(Device)) { + pdiskInfo.push({ + label: pDiskInfoKeyset('device'), + value: , + }); + } + if (valueIsDefined(Realtime)) { + pdiskInfo.push({ + label: pDiskInfoKeyset('realtime'), + value: , + }); + } + if (valueIsDefined(SerialNumber)) { + pdiskInfo.push({ + label: pDiskInfoKeyset('serial-number'), + value: SerialNumber || EMPTY_DATA_PLACEHOLDER, + }); + } + + if (valueIsDefined(PDiskId) && valueIsDefined(nodeId)) { + const pDiskPagePath = getPDiskPagePath(PDiskId, nodeId); + const pDiskInternalViewerPath = createPDiskDeveloperUILink({ + nodeId, + pDiskId: PDiskId, + }); + + pdiskInfo.push({ + label: pDiskInfoKeyset('links'), + value: ( + + {!isPDiskPage && ( + + )} + + + ), + }); + } + + return ; +} diff --git a/src/components/PDiskInfo/i18n/en.json b/src/components/PDiskInfo/i18n/en.json new file mode 100644 index 000000000..54cfde02f --- /dev/null +++ b/src/components/PDiskInfo/i18n/en.json @@ -0,0 +1,15 @@ +{ + "path": "Path", + "guid": "GUID", + "category": "Category", + "type": "Type", + "size": "Size", + "state": "State", + "device": "Device", + "realtime": "Realtime", + "serial-number": "SerialNumber", + "links": "Links", + + "developer-ui": "Developer UI", + "pdisk-page": "PDisk page" +} diff --git a/src/components/PDiskInfo/i18n/index.ts b/src/components/PDiskInfo/i18n/index.ts new file mode 100644 index 000000000..fc7735e03 --- /dev/null +++ b/src/components/PDiskInfo/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-pDisk-info'; + +export const pDiskInfoKeyset = registerKeysets(COMPONENT, {en}); diff --git a/src/components/PageMeta/PageMeta.scss b/src/components/PageMeta/PageMeta.scss new file mode 100644 index 000000000..7cffe23e7 --- /dev/null +++ b/src/components/PageMeta/PageMeta.scss @@ -0,0 +1,10 @@ +@import '../../styles//mixins.scss'; + +.ydb-page-meta { + display: flex; + flex-flow: row nowrap; + + color: var(--g-color-text-primary); + + @include body-2-typography(); +} diff --git a/src/components/PageMeta/PageMeta.tsx b/src/components/PageMeta/PageMeta.tsx new file mode 100644 index 000000000..17f9e03c4 --- /dev/null +++ b/src/components/PageMeta/PageMeta.tsx @@ -0,0 +1,17 @@ +import {cn} from '../../utils/cn'; +import './PageMeta.scss'; + +const b = cn('ydb-page-meta'); + +interface PageMetaProps { + items: (string | undefined)[]; + className?: string; +} + +export function PageMeta({items, className}: PageMetaProps) { + return ( +
+ {items.filter((item) => Boolean(item)).join('\u00a0\u00a0\u00B7\u00a0\u00a0')} +
+ ); +} diff --git a/src/components/StatusIcon/StatusIcon.scss b/src/components/StatusIcon/StatusIcon.scss new file mode 100644 index 000000000..255370c59 --- /dev/null +++ b/src/components/StatusIcon/StatusIcon.scss @@ -0,0 +1,69 @@ +.ydb-status-icon { + &__status-color { + &_state_green { + background-color: var(--ydb-color-status-green); + } + &_state_yellow { + background-color: var(--ydb-color-status-yellow); + } + &_state_blue { + background-color: var(--ydb-color-status-blue); + } + &_state_red { + background-color: var(--ydb-color-status-red); + } + &_state_grey { + background-color: var(--ydb-color-status-grey); + } + &_state_orange { + background-color: var(--ydb-color-status-orange); + } + } + + &__status-icon { + &_state_blue { + color: var(--ydb-color-status-blue); + } + &_state_yellow { + color: var(--ydb-color-status-yellow); + } + &_state_orange { + color: var(--ydb-color-status-orange); + } + &_state_red { + color: var(--ydb-color-status-red); + } + } + + &__status-color, + &__status-icon { + flex-shrink: 0; + + margin-right: 8px; + + border-radius: 3px; + &_size_xs { + aspect-ratio: 1; + + width: 12px; + height: 12px; + } + &_size_s { + aspect-ratio: 1; + + width: 16px; + height: 16px; + } + &_size_m { + aspect-ratio: 1; + + width: 18px; + height: 18px; + } + + &_size_l { + width: 27px; + height: 27px; + } + } +} diff --git a/src/components/StatusIcon/StatusIcon.tsx b/src/components/StatusIcon/StatusIcon.tsx new file mode 100644 index 000000000..b63636bf4 --- /dev/null +++ b/src/components/StatusIcon/StatusIcon.tsx @@ -0,0 +1,42 @@ +import {Icon} from '@gravity-ui/uikit'; + +import circleExclamationIcon from '../../assets/icons/circle-exclamation.svg'; +import circleInfoIcon from '../../assets/icons/circle-info.svg'; +import circleTimesIcon from '../../assets/icons/circle-xmark.svg'; +import triangleExclamationIcon from '../../assets/icons/triangle-exclamation.svg'; +import {EFlag} from '../../types/api/enums'; +import {cn} from '../../utils/cn'; +import './StatusIcon.scss'; + +const b = cn('ydb-status-icon'); + +const icons = { + [EFlag.Blue]: circleInfoIcon, + [EFlag.Yellow]: circleExclamationIcon, + [EFlag.Orange]: triangleExclamationIcon, + [EFlag.Red]: circleTimesIcon, +}; + +export type StatusIconMode = 'color' | 'icons'; +export type StatusIconSize = 'xs' | 's' | 'm' | 'l'; + +interface StatusIconProps { + status?: EFlag; + size?: StatusIconSize; + mode?: StatusIconMode; +} + +export function StatusIcon({status = EFlag.Grey, size = 's', mode = 'color'}: StatusIconProps) { + const modifiers = {state: status.toLowerCase(), size}; + + if (mode === 'icons' && status in icons) { + return ( + + ); + } + + return
; +} diff --git a/src/containers/App/Content.tsx b/src/containers/App/Content.tsx index 422996353..1f85037cf 100644 --- a/src/containers/App/Content.tsx +++ b/src/containers/App/Content.tsx @@ -9,6 +9,7 @@ import {Clusters} from '../Clusters/Clusters'; import Cluster from '../Cluster/Cluster'; import Tenant from '../Tenant/Tenant'; import Node from '../Node/Node'; +import {PDisk} from '../PDisk/PDisk'; import {Tablet} from '../Tablet'; import TabletsFilters from '../TabletsFilters/TabletsFilters'; import Header from '../Header/Header'; @@ -22,6 +23,7 @@ import { ClusterSlot, ClustersSlot, NodeSlot, + PDiskSlot, RedirectSlot, RoutesSlot, TabletSlot, @@ -61,6 +63,11 @@ const routesSlots: RouteSlot[] = [ slot: NodeSlot, component: Node, }, + { + path: routes.pDisk, + slot: PDiskSlot, + component: PDisk, + }, { path: routes.tablet, slot: TabletSlot, diff --git a/src/containers/App/appSlots.tsx b/src/containers/App/appSlots.tsx index 9862b637b..68f2955bb 100644 --- a/src/containers/App/appSlots.tsx +++ b/src/containers/App/appSlots.tsx @@ -4,6 +4,7 @@ import type {RedirectProps, RouteComponentProps} from 'react-router'; import type Cluster from '../Cluster/Cluster'; import type {Clusters} from '../Clusters/Clusters'; import type Node from '../Node/Node'; +import type {PDisk} from '../PDisk/PDisk'; import type {Tablet} from '../Tablet'; import type TabletsFilters from '../TabletsFilters/TabletsFilters'; import type Tenant from '../Tenant/Tenant'; @@ -28,6 +29,11 @@ export const NodeSlot = createSlot<{ | React.ReactNode | ((props: {component: typeof Node} & RouteComponentProps) => React.ReactNode); }>('node'); +export const PDiskSlot = createSlot<{ + children: + | React.ReactNode + | ((props: {component: typeof PDisk} & RouteComponentProps) => React.ReactNode); +}>('pDisk'); export const TabletSlot = createSlot<{ children: | React.ReactNode diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss index 6e6b02b53..a98be143b 100644 --- a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss +++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss @@ -3,6 +3,10 @@ .cluster-info { padding-top: 20px; + &__skeleton { + margin-top: 5px; + } + &__error { @include body-2-typography(); } diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx index 51c304c21..948d73428 100644 --- a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx +++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx @@ -5,8 +5,9 @@ import InfoViewer, {InfoViewerItem} from '../../../components/InfoViewer/InfoVie import {Tags} from '../../../components/Tags'; import {Tablet} from '../../../components/Tablet'; import {ResponseError} from '../../../components/Errors/ResponseError'; -import {ExternalLinkWithIcon} from '../../../components/ExternalLinkWithIcon/ExternalLinkWithIcon'; +import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon'; import {ContentWithPopup} from '../../../components/ContentWithPopup/ContentWithPopup'; +import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton'; import type {IResponseError} from '../../../types/api/error'; import type {AdditionalClusterProps, ClusterLink} from '../../../types/additionalProps'; @@ -24,7 +25,6 @@ import type { } from '../../../store/reducers/cluster/types'; import {VersionsBar} from '../VersionsBar/VersionsBar'; -import {ClusterInfoSkeleton} from '../ClusterInfoSkeleton/ClusterInfoSkeleton'; import i18n from '../i18n'; import {compareTablets} from './utils'; @@ -176,7 +176,7 @@ const getInfo = ( value: (
{links.map(({title, url}) => ( - + ))}
), @@ -224,7 +224,7 @@ export const ClusterInfo = ({ const getContent = () => { if (loading) { - return ; + return ; } if (error) { diff --git a/src/containers/Header/Header.tsx b/src/containers/Header/Header.tsx index 7cf9b1db5..8a1e53395 100644 --- a/src/containers/Header/Header.tsx +++ b/src/containers/Header/Header.tsx @@ -4,7 +4,7 @@ import block from 'bem-cn-lite'; import {Breadcrumbs} from '@gravity-ui/uikit'; -import {ExternalLinkWithIcon} from '../../components/ExternalLinkWithIcon/ExternalLinkWithIcon'; +import {LinkWithIcon} from '../../components/LinkWithIcon/LinkWithIcon'; import {backend, customBackend} from '../../store'; import {getClusterInfo} from '../../store/reducers/cluster/cluster'; @@ -98,10 +98,7 @@ function Header({mainPage}: HeaderProps) { />
- + ); }; diff --git a/src/containers/Header/breadcrumbs.tsx b/src/containers/Header/breadcrumbs.tsx index 90cf51076..566c3e339 100644 --- a/src/containers/Header/breadcrumbs.tsx +++ b/src/containers/Header/breadcrumbs.tsx @@ -9,6 +9,7 @@ import type { BreadcrumbsOptions, ClusterBreadcrumbsOptions, NodeBreadcrumbsOptions, + PDiskBreadcrumbsOptions, Page, TabletBreadcrumbsOptions, TabletsBreadcrumbsOptions, @@ -20,12 +21,13 @@ import { TENANT_PAGES_IDS, } from '../../store/reducers/tenant/constants'; import {TabletIcon} from '../../components/TabletIcon/TabletIcon'; -import routes, {createHref} from '../../routes'; +import routes, {createHref, getPDiskPagePath} from '../../routes'; import {CLUSTER_DEFAULT_TITLE, getTabletLabel} from '../../utils/constants'; import {getClusterPath} from '../Cluster/utils'; import {TenantTabsGroups, getTenantPath} from '../Tenant/TenantPages'; import {getDefaultNodePath} from '../Node/NodePages'; +import {headerKeyset} from './i18n'; const prepareTenantName = (tenantName: string) => { return tenantName.startsWith('/') ? tenantName.slice(1) : tenantName; @@ -58,7 +60,7 @@ const getTenantBreadcrumbs = ( ): RawBreadcrumbItem[] => { const {tenantName} = options; - const text = tenantName ? prepareTenantName(tenantName) : 'Tenant'; + const text = tenantName ? prepareTenantName(tenantName) : headerKeyset('breadcrumbs.tenant'); const link = tenantName ? getTenantPath({...query, name: tenantName}) : undefined; return [...getClusterBreadcrumbs(options, query), {text, link, icon: }]; @@ -84,7 +86,9 @@ const getNodeBreadcrumbs = (options: NodeBreadcrumbsOptions, query = {}): RawBre breadcrumbs = getTenantBreadcrumbs(options, newQuery); } - const text = nodeId ? `Node ${nodeId}` : 'Node'; + const text = nodeId + ? `${headerKeyset('breadcrumbs.node')} ${nodeId}` + : headerKeyset('breadcrumbs.node'); const link = nodeId ? getDefaultNodePath(nodeId, query) : undefined; const icon = isStorageNode ? : ; @@ -97,6 +101,28 @@ const getNodeBreadcrumbs = (options: NodeBreadcrumbsOptions, query = {}): RawBre return breadcrumbs; }; +const getPDiskBreadcrumbs = (options: PDiskBreadcrumbsOptions, query = {}) => { + const {nodeId, pDiskId} = options; + + const breadcrumbs = getNodeBreadcrumbs({ + // PDisks relate to storage Nodes, they don't have tenant name + tenantName: undefined, + nodeId: nodeId, + }); + + const text = pDiskId + ? `${headerKeyset('breadcrumbs.pDisk')} ${pDiskId}` + : headerKeyset('breadcrumbs.pDisk'); + const link = pDiskId && nodeId ? getPDiskPagePath(pDiskId, nodeId, query) : undefined; + + breadcrumbs.push({ + text, + link, + }); + + return breadcrumbs; +}; + const getTabletsBreadcrubms = ( options: TabletsBreadcrumbsOptions, query = {}, @@ -124,7 +150,7 @@ const getTabletsBreadcrubms = ( path: tenantName, }); - breadcrumbs.push({text: 'Tablets', link}); + breadcrumbs.push({text: headerKeyset('breadcrumbs.tablets'), link}); return breadcrumbs; }; @@ -138,7 +164,7 @@ const getTabletBreadcrubms = ( const breadcrumbs = getTabletsBreadcrubms(options, query); breadcrumbs.push({ - text: tabletId || 'Tablet', + text: tabletId || headerKeyset('breadcrumbs.tablet'), icon: , }); @@ -161,6 +187,9 @@ export const getBreadcrumbs = ( case 'node': { return [...rawBreadcrumbs, ...getNodeBreadcrumbs(options, query)]; } + case 'pDisk': { + return [...rawBreadcrumbs, ...getPDiskBreadcrumbs(options, query)]; + } case 'tablets': { return [...rawBreadcrumbs, ...getTabletsBreadcrubms(options, query)]; } diff --git a/src/containers/Header/i18n/en.json b/src/containers/Header/i18n/en.json new file mode 100644 index 000000000..2715bd53f --- /dev/null +++ b/src/containers/Header/i18n/en.json @@ -0,0 +1,7 @@ +{ + "breadcrumbs.tenant": "Tenant", + "breadcrumbs.node": "Node", + "breadcrumbs.pDisk": "PDisk", + "breadcrumbs.tablet": "Tablet", + "breadcrumbs.tablets": "Tablets" +} diff --git a/src/containers/Header/i18n/index.ts b/src/containers/Header/i18n/index.ts new file mode 100644 index 000000000..caccbd9ca --- /dev/null +++ b/src/containers/Header/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-header'; + +export const headerKeyset = registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Node/NodeStructure/NodeStructure.tsx b/src/containers/Node/NodeStructure/NodeStructure.tsx index e25458a51..5c9fddf64 100644 --- a/src/containers/Node/NodeStructure/NodeStructure.tsx +++ b/src/containers/Node/NodeStructure/NodeStructure.tsx @@ -18,10 +18,6 @@ import './NodeStructure.scss'; const b = cn('kv-node-structure'); -export function valueIsDefined(value: T | null | undefined): value is T { - return value !== null && value !== undefined; -} - function generateId({type, id}: {type: 'pdisk' | 'vdisk'; id: string}) { return `${type}-${id}`; } diff --git a/src/containers/Node/NodeStructure/Pdisk.tsx b/src/containers/Node/NodeStructure/Pdisk.tsx index 806275bba..18856b324 100644 --- a/src/containers/Node/NodeStructure/Pdisk.tsx +++ b/src/containers/Node/NodeStructure/Pdisk.tsx @@ -13,21 +13,17 @@ import type { PreparedStructureVDisk, } from '../../../store/reducers/node/types'; import {EVDiskState} from '../../../types/api/vdisk'; -import {bytesToGB} from '../../../utils/utils'; import {formatStorageValuesToGb} from '../../../utils/dataFormatters/dataFormatters'; import {DEFAULT_TABLE_SETTINGS} from '../../../utils/constants'; -import { - createPDiskDeveloperUILink, - createVDiskDeveloperUILink, -} from '../../../utils/developerUI/developerUI'; +import {valueIsDefined} from '../../../utils'; +import {createVDiskDeveloperUILink} from '../../../utils/developerUI/developerUI'; import {EntityStatus} from '../../../components/EntityStatus/EntityStatus'; -import InfoViewer, {type InfoViewerItem} from '../../../components/InfoViewer/InfoViewer'; import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer'; import {Icon} from '../../../components/Icon'; +import {PDiskInfo} from '../../../components/PDiskInfo/PDiskInfo'; import i18n from '../i18n'; import {Vdisk} from './Vdisk'; -import {valueIsDefined} from './NodeStructure'; import {PDiskTitleBadge} from './PDiskTitleBadge'; const b = cn('kv-node-structure'); @@ -173,20 +169,7 @@ export function PDisk({ }: PDiskProps) { const [unfolded, setUnfolded] = useState(unfoldedFromProps ?? false); - const { - TotalSize = 0, - AvailableSize = 0, - Device, - Guid, - PDiskId, - Path, - Realtime, - State, - Category, - Type, - SerialNumber, - vDisks, - } = data; + const {TotalSize = 0, AvailableSize = 0, Device, PDiskId, Type, vDisks} = data; const total = Number(TotalSize); const available = Number(AvailableSize); @@ -216,90 +199,10 @@ export function PDisk({ if (isEmpty(data)) { return
No information about PDisk
; } - let pDiskInternalViewerLink = null; - if (valueIsDefined(PDiskId) && valueIsDefined(nodeId)) { - pDiskInternalViewerLink = createPDiskDeveloperUILink({ - nodeId, - pDiskId: PDiskId, - }); - } - - const pdiskInfo: InfoViewerItem[] = [ - { - label: 'PDisk Id', - value: ( -
- {PDiskId} - {pDiskInternalViewerLink && ( - - )} -
- ), - }, - ]; - if (valueIsDefined(Path)) { - pdiskInfo.push({label: 'Path', value: Path}); - } - if (valueIsDefined(Guid)) { - pdiskInfo.push({label: 'GUID', value: Guid}); - } - if (valueIsDefined(Category)) { - pdiskInfo.push({label: 'Category', value: Category}); - pdiskInfo.push({label: 'Type', value: Type}); - } - pdiskInfo.push({ - label: 'Allocated Size', - value: bytesToGB(total - available), - }); - pdiskInfo.push({ - label: 'Available Size', - value: bytesToGB(available), - }); - if (total >= 0 && available >= 0) { - pdiskInfo.push({ - label: 'Size', - value: ( - - ), - }); - } - if (valueIsDefined(State)) { - pdiskInfo.push({label: 'State', value: State}); - } - if (valueIsDefined(Device)) { - pdiskInfo.push({ - label: 'Device', - value: , - }); - } - if (valueIsDefined(Realtime)) { - pdiskInfo.push({ - label: 'Realtime', - value: , - }); - } - if (valueIsDefined(SerialNumber)) { - pdiskInfo.push({label: 'SerialNumber', value: SerialNumber}); - } return (
- +
VDisks
{renderVDisks()} diff --git a/src/containers/Node/NodeStructure/Vdisk.tsx b/src/containers/Node/NodeStructure/Vdisk.tsx index ca6626696..3c8c1ddc3 100644 --- a/src/containers/Node/NodeStructure/Vdisk.tsx +++ b/src/containers/Node/NodeStructure/Vdisk.tsx @@ -8,12 +8,11 @@ import { stringifyVdiskId, } from '../../../utils/dataFormatters/dataFormatters'; import {bytesToGB, bytesToSpeed} from '../../../utils/utils'; +import {valueIsDefined} from '../../../utils'; import {EntityStatus} from '../../../components/EntityStatus/EntityStatus'; import InfoViewer from '../../../components/InfoViewer/InfoViewer'; import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer'; -import {valueIsDefined} from './NodeStructure'; - const b = cn('kv-node-structure'); export function Vdisk({ diff --git a/src/containers/PDisk/PDisk.scss b/src/containers/PDisk/PDisk.scss new file mode 100644 index 000000000..89525b07e --- /dev/null +++ b/src/containers/PDisk/PDisk.scss @@ -0,0 +1,41 @@ +@import '../../styles//mixins.scss'; + +.ydb-pdisk-page { + position: relative; + + display: flex; + overflow: auto; + flex-direction: column; + + height: 100%; + padding-top: 20px; + padding-left: 20px; + + gap: 20px; + + &__meta, + &__title, + &__info, + &__groups-title { + position: sticky; + left: 0; + } + + &__title { + display: flex; + flex-flow: row nowrap; + align-items: baseline; + + @include header-2-typography(); + + &__prefix { + margin-right: 6px; + + color: var(--g-color-text-secondary); + } + } + + &__groups-title { + @include header-1-typography(); + } +} diff --git a/src/containers/PDisk/PDisk.tsx b/src/containers/PDisk/PDisk.tsx new file mode 100644 index 000000000..5b8ac7cf0 --- /dev/null +++ b/src/containers/PDisk/PDisk.tsx @@ -0,0 +1,133 @@ +import {useCallback, useEffect} from 'react'; +import {StringParam, useQueryParams} from 'use-query-params'; +import {Helmet} from 'react-helmet-async'; + +import { + getPDiskData, + getPDiskStorage, + setPDiskDataWasNotLoaded, +} from '../../store/reducers/pdisk/pdisk'; +import {setHeaderBreadcrumbs} from '../../store/reducers/header/header'; +import {getNodesList, selectNodesMap} from '../../store/reducers/nodesList'; + +import {useAutofetcher, useTypedDispatch, useTypedSelector} from '../../utils/hooks'; +import {getSeverityColor} from '../../utils/disks/helpers'; + +import {PageMeta} from '../../components/PageMeta/PageMeta'; +import {StatusIcon} from '../../components/StatusIcon/StatusIcon'; +import {PDiskInfo} from '../../components/PDiskInfo/PDiskInfo'; +import {InfoViewerSkeleton} from '../../components/InfoViewerSkeleton/InfoViewerSkeleton'; + +import {PDiskGroups} from './PDiskGroups'; +import {pdiskPageCn} from './shared'; +import {pDiskPageKeyset} from './i18n'; + +import './PDisk.scss'; + +export function PDisk() { + const dispatch = useTypedDispatch(); + + const nodesMap = useTypedSelector(selectNodesMap); + const {pDiskData, groupsData, pDiskLoading, pDiskWasLoaded, groupsLoading, groupsWasLoaded} = + useTypedSelector((state) => state.pDisk); + const {NodeHost, NodeId, NodeType, NodeDC, Severity} = pDiskData; + + const [{nodeId, pDiskId}] = useQueryParams({ + nodeId: StringParam, + pDiskId: StringParam, + }); + + useEffect(() => { + dispatch(setHeaderBreadcrumbs('pDisk', {nodeId, pDiskId})); + }, [dispatch, nodeId, pDiskId]); + + useEffect(() => { + dispatch(getNodesList()); + }, [dispatch]); + + const fetchData = useCallback( + (isBackground: boolean) => { + if (!isBackground) { + dispatch(setPDiskDataWasNotLoaded()); + } + if (nodeId && pDiskId) { + dispatch(getPDiskData({nodeId, pDiskId})); + dispatch(getPDiskStorage({nodeId, pDiskId})); + } + }, + [dispatch, nodeId, pDiskId], + ); + + useAutofetcher(fetchData, [fetchData], true); + + const renderHelmet = () => { + const pDiskPagePart = pDiskId + ? `${pDiskPageKeyset('pdisk')} ${pDiskId}` + : pDiskPageKeyset('pdisk'); + + const nodePagePart = NodeHost ? NodeHost : pDiskPageKeyset('node'); + + return ( + + ); + }; + + const renderPageMeta = () => { + const hostItem = NodeHost ? `${pDiskPageKeyset('fqdn')}: ${NodeHost}` : undefined; + const nodeIdItem = NodeId ? `${pDiskPageKeyset('node')}: ${NodeId}` : undefined; + + return ( + + ); + }; + + const renderPageTitle = () => { + return ( +
+ {pDiskPageKeyset('pdisk')} + + {pDiskId} +
+ ); + }; + + const renderInfo = () => { + if (pDiskLoading && !pDiskWasLoaded) { + return ; + } + return ( + + ); + }; + + const renderGroupsTable = () => { + return ( + + ); + }; + + return ( +
+ {renderHelmet()} + {renderPageMeta()} + {renderPageTitle()} + {renderInfo()} + {renderGroupsTable()} +
+ ); +} diff --git a/src/containers/PDisk/PDiskGroups.tsx b/src/containers/PDisk/PDiskGroups.tsx new file mode 100644 index 000000000..80bfd4a9b --- /dev/null +++ b/src/containers/PDisk/PDiskGroups.tsx @@ -0,0 +1,49 @@ +import {useMemo} from 'react'; + +import DataTable from '@gravity-ui/react-data-table'; + +import type {PreparedStorageGroup} from '../../store/reducers/storage/types'; +import type {NodesMap} from '../../types/store/nodesList'; + +import {TableSkeleton} from '../../components/TableSkeleton/TableSkeleton'; + +import {DEFAULT_TABLE_SETTINGS} from '../../utils/constants'; + +import {getPDiskStorageColumns} from '../Storage/StorageGroups/getStorageGroupsColumns'; + +import {pdiskPageCn} from './shared'; +import {pDiskPageKeyset} from './i18n'; + +interface PDiskGroupsProps { + data: PreparedStorageGroup[]; + nodesMap?: NodesMap; + loading?: boolean; +} + +export function PDiskGroups({data, nodesMap, loading}: PDiskGroupsProps) { + const pDiskStorageColumns = useMemo(() => { + return getPDiskStorageColumns(nodesMap); + }, [nodesMap]); + + const renderContent = () => { + if (loading) { + return ; + } + + return ( + + ); + }; + + return ( + <> +
{pDiskPageKeyset('groups')}
+
{renderContent()}
+ + ); +} diff --git a/src/containers/PDisk/i18n/en.json b/src/containers/PDisk/i18n/en.json new file mode 100644 index 000000000..197ed9608 --- /dev/null +++ b/src/containers/PDisk/i18n/en.json @@ -0,0 +1,6 @@ +{ + "fqdn": "FQDN", + "pdisk": "PDisk", + "groups": "Groups", + "node": "Node" +} diff --git a/src/containers/PDisk/i18n/index.ts b/src/containers/PDisk/i18n/index.ts new file mode 100644 index 000000000..86065fddf --- /dev/null +++ b/src/containers/PDisk/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-pDisk-page'; + +export const pDiskPageKeyset = registerKeysets(COMPONENT, {en}); diff --git a/src/containers/PDisk/shared.ts b/src/containers/PDisk/shared.ts new file mode 100644 index 000000000..27792b5d2 --- /dev/null +++ b/src/containers/PDisk/shared.ts @@ -0,0 +1,3 @@ +import {cn} from '../../utils/cn'; + +export const pdiskPageCn = cn('ydb-pdisk-page'); diff --git a/src/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx b/src/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx index 84409540a..a259c72cc 100644 --- a/src/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx +++ b/src/containers/Storage/DiskStateProgressBar/DiskStateProgressBar.tsx @@ -1,22 +1,14 @@ import React from 'react'; import cn from 'bem-cn-lite'; -import {DISK_COLOR_STATE_TO_NUMERIC_SEVERITY} from '../../../utils/disks/constants'; import {INVERTED_DISKS_KEY} from '../../../utils/constants'; import {useSetting} from '../../../utils/hooks'; +import {getSeverityColor} from '../../../utils/disks/helpers'; import './DiskStateProgressBar.scss'; const b = cn('storage-disk-progress-bar'); -type Color = keyof typeof DISK_COLOR_STATE_TO_NUMERIC_SEVERITY; - -type SeverityToColor = Record; - -const severityToColor = Object.entries( - DISK_COLOR_STATE_TO_NUMERIC_SEVERITY, -).reduce((acc, [color, severity]) => ({...acc, [severity]: color as Color}), {}); - interface DiskStateProgressBarProps { diskAllocatedPercent?: number; severity?: number; @@ -56,7 +48,7 @@ export function DiskStateProgressBar({ const mods: Record = {inverted, compact}; - const color = severity !== undefined && severityToColor[severity]; + const color = severity !== undefined && getSeverityColor(severity); if (color) { mods[color.toLocaleLowerCase()] = true; } diff --git a/src/containers/Storage/StorageGroups/getStorageGroupsColumns.tsx b/src/containers/Storage/StorageGroups/getStorageGroupsColumns.tsx index 904dce3c4..680473ce2 100644 --- a/src/containers/Storage/StorageGroups/getStorageGroupsColumns.tsx +++ b/src/containers/Storage/StorageGroups/getStorageGroupsColumns.tsx @@ -247,6 +247,19 @@ export const getStorageTopGroupsColumns = (): StorageGroupsColumn[] => { return [groupIdColumn, kindColumn, erasureColumn, usageColumn, usedColumn, limitColumn]; }; +export const getPDiskStorageColumns = (nodes?: NodesMap): StorageGroupsColumn[] => { + return [ + poolNameColumn, + kindColumn, + erasureColumn, + degradedColumn, + groupIdColumn, + usageColumn, + usedColumn, + getVdiscksColumn(nodes), + ]; +}; + const getStorageGroupsColumns = (nodes?: NodesMap): StorageGroupsColumn[] => { return [ poolNameColumn, diff --git a/src/containers/Tenant/Info/ExternalTable/ExternalTable.tsx b/src/containers/Tenant/Info/ExternalTable/ExternalTable.tsx index 836accd8d..cc9ff47fd 100644 --- a/src/containers/Tenant/Info/ExternalTable/ExternalTable.tsx +++ b/src/containers/Tenant/Info/ExternalTable/ExternalTable.tsx @@ -6,7 +6,7 @@ import {useTypedSelector} from '../../../../utils/hooks'; import {createExternalUILink, parseQuery} from '../../../../routes'; import {formatCommonItem} from '../../../../components/InfoViewer/formatters'; import {InfoViewer, InfoViewerItem} from '../../../../components/InfoViewer'; -import {ExternalLinkWithIcon} from '../../../../components/ExternalLinkWithIcon/ExternalLinkWithIcon'; +import {LinkWithIcon} from '../../../../components/LinkWithIcon/LinkWithIcon'; import {EntityStatus} from '../../../../components/EntityStatus/EntityStatus'; import {ResponseError} from '../../../../components/Errors/ResponseError'; @@ -33,7 +33,7 @@ const prepareExternalTableSummary = ( label: i18n('external-objects.data-source'), value: DataSourcePath && ( - + ), }, diff --git a/src/routes.ts b/src/routes.ts index e8222c6ef..73c71f907 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -9,6 +9,7 @@ export const CLUSTERS = 'clusters'; export const CLUSTER = 'cluster'; export const TENANT = 'tenant'; export const NODE = 'node'; +export const PDISK = 'pDisk'; export const TABLET = 'tablet'; const routes = { @@ -16,11 +17,14 @@ const routes = { cluster: `/${CLUSTER}/:activeTab?`, tenant: `/${TENANT}`, node: `/${NODE}/:id/:activeTab?`, + pDisk: `/${PDISK}`, tablet: `/${TABLET}/:id`, tabletsFilters: `/tabletsFilters`, auth: `/auth`, } as const; +export default routes; + export const parseQuery = (location: Location) => { return qs.parse(location.search, { ignoreQueryPrefix: true, @@ -81,4 +85,10 @@ export function getLocationObjectFromHref(href: string) { return {pathname, search, hash}; } -export default routes; +export function getPDiskPagePath( + pDiskId: string | number, + nodeId: string | number, + query: Query = {}, +) { + return createHref(routes.pDisk, undefined, {...query, nodeId, pDiskId}); +} diff --git a/src/services/api.ts b/src/services/api.ts index b3e7a877d..f40f69c7a 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -69,7 +69,7 @@ export class YdbEmbeddedAPI extends AxiosWrapper { {concurrentId: concurrentId || `getClusterNodes`}, ); } - getNodeInfo(id?: string) { + getNodeInfo(id?: string | number) { return this.get(this.getPath('/viewer/json/sysinfo?enums=true'), { node_id: id, }); diff --git a/src/store/reducers/header/types.ts b/src/store/reducers/header/types.ts index de75357bc..f8332e7fc 100644 --- a/src/store/reducers/header/types.ts +++ b/src/store/reducers/header/types.ts @@ -3,7 +3,7 @@ import type {EType} from '../../../types/api/tablet'; import {setHeaderBreadcrumbs} from './header'; -export type Page = 'cluster' | 'tenant' | 'node' | 'tablets' | 'tablet' | undefined; +export type Page = 'cluster' | 'tenant' | 'node' | 'pDisk' | 'tablets' | 'tablet' | undefined; export interface ClusterBreadcrumbsOptions { clusterName?: string; @@ -18,6 +18,10 @@ export interface NodeBreadcrumbsOptions extends TenantBreadcrumbsOptions { nodeId?: string | number; } +export interface PDiskBreadcrumbsOptions extends Omit { + pDiskId?: string | number; +} + export interface TabletsBreadcrumbsOptions extends TenantBreadcrumbsOptions { nodeIds?: string[] | number[]; } diff --git a/src/store/reducers/index.ts b/src/store/reducers/index.ts index b2f11fbcd..3700f6eef 100644 --- a/src/store/reducers/index.ts +++ b/src/store/reducers/index.ts @@ -21,6 +21,7 @@ import tenants from './tenants/tenants'; import tablet from './tablet'; import topic from './topic'; import partitions from './partitions/partitions'; +import pDisk from './pdisk/pdisk'; import executeQuery from './executeQuery'; import explainQuery from './explainQuery'; import tabletsFilters from './tabletsFilters'; @@ -67,6 +68,7 @@ export const rootReducer = { tablet, topic, partitions, + pDisk, executeQuery, explainQuery, tabletsFilters, diff --git a/src/store/reducers/nodes/types.ts b/src/store/reducers/nodes/types.ts index 3975089f8..1c6c57184 100644 --- a/src/store/reducers/nodes/types.ts +++ b/src/store/reducers/nodes/types.ts @@ -80,6 +80,7 @@ export interface NodesGeneralRequestParams extends NodesSortParams { } export interface NodesApiRequestParams extends NodesGeneralRequestParams { + node_id?: number | string; // get only specific node path?: string; tenant?: string; type?: NodeType; diff --git a/src/store/reducers/pdisk/pdisk.ts b/src/store/reducers/pdisk/pdisk.ts new file mode 100644 index 000000000..64d57fe92 --- /dev/null +++ b/src/store/reducers/pdisk/pdisk.ts @@ -0,0 +1,116 @@ +import type {Reducer} from '@reduxjs/toolkit'; + +import {EVersion} from '../../../types/api/storage'; +import {createRequestActionTypes, createApiRequest} from '../../utils'; +import type {PDiskAction, PDiskState} from './types'; +import {preparePDiksDataResponse, preparePDiskStorageResponse} from './utils'; + +export const FETCH_PDISK = createRequestActionTypes('pdisk', 'FETCH_PDISK'); +export const FETCH_PDISK_GROUPS = createRequestActionTypes('pdisk', 'FETCH_PDISK_GROUPS'); +const SET_PDISK_DATA_WAS_NOT_LOADED = 'pdisk/SET_PDISK_DATA_WAS_NOT_LOADED'; + +const initialState = { + pDiskLoading: false, + pDiskWasLoaded: false, + pDiskData: {}, + groupsLoading: false, + groupsWasLoaded: false, + groupsData: [], +}; + +const pdisk: Reducer = (state = initialState, action) => { + switch (action.type) { + case FETCH_PDISK.REQUEST: { + return { + ...state, + pDiskLoading: true, + }; + } + case FETCH_PDISK.SUCCESS: { + return { + ...state, + pDiskData: action.data, + pDiskLoading: false, + pDiskWasLoaded: true, + pDiskError: undefined, + }; + } + case FETCH_PDISK.FAILURE: { + return { + ...state, + pDiskError: action.error, + pDiskLoading: false, + }; + } + case FETCH_PDISK_GROUPS.REQUEST: { + return { + ...state, + groupsLoading: true, + }; + } + case FETCH_PDISK_GROUPS.SUCCESS: { + return { + ...state, + groupsData: action.data, + groupsLoading: false, + groupsWasLoaded: true, + groupsError: undefined, + }; + } + case FETCH_PDISK_GROUPS.FAILURE: { + return { + ...state, + groupsError: action.error, + groupsLoading: false, + }; + } + case SET_PDISK_DATA_WAS_NOT_LOADED: { + return { + ...state, + pDiskWasLoaded: false, + groupsWasLoaded: false, + }; + } + default: + return state; + } +}; + +export const setPDiskDataWasNotLoaded = () => { + return { + type: SET_PDISK_DATA_WAS_NOT_LOADED, + } as const; +}; + +export const getPDiskData = ({ + nodeId, + pDiskId, +}: { + nodeId: number | string; + pDiskId: number | string; +}) => { + return createApiRequest({ + request: Promise.all([ + window.api.getPdiskInfo(nodeId, pDiskId), + window.api.getNodeInfo(nodeId), + ]), + actions: FETCH_PDISK, + dataHandler: preparePDiksDataResponse, + }); +}; + +export const getPDiskStorage = ({ + nodeId, + pDiskId, +}: { + nodeId: number | string; + pDiskId: number | string; +}) => { + return createApiRequest({ + request: window.api.getStorageInfo({nodeId, version: EVersion.v1}), + actions: FETCH_PDISK_GROUPS, + dataHandler: (data) => preparePDiskStorageResponse(data, pDiskId, nodeId), + }); +}; + +export default pdisk; diff --git a/src/store/reducers/pdisk/types.ts b/src/store/reducers/pdisk/types.ts new file mode 100644 index 000000000..b3a728fab --- /dev/null +++ b/src/store/reducers/pdisk/types.ts @@ -0,0 +1,29 @@ +import type {IResponseError} from '../../../types/api/error'; +import type {PreparedPDisk} from '../../../utils/disks/types'; +import type {ApiRequestAction} from '../../utils'; +import type {PreparedStorageGroup} from '../storage/types'; +import type {FETCH_PDISK, FETCH_PDISK_GROUPS, setPDiskDataWasNotLoaded} from './pdisk'; + +interface PDiskData extends PreparedPDisk { + NodeId?: number; + NodeHost?: string; + NodeType?: string; + NodeDC?: string; +} + +export interface PDiskState { + pDiskLoading: boolean; + pDiskWasLoaded: boolean; + pDiskData: PDiskData; + pDiskError?: IResponseError; + + groupsLoading: boolean; + groupsWasLoaded: boolean; + groupsData: PreparedStorageGroup[]; + groupsError?: IResponseError; +} + +export type PDiskAction = + | ApiRequestAction + | ApiRequestAction + | ReturnType; diff --git a/src/store/reducers/pdisk/utils.ts b/src/store/reducers/pdisk/utils.ts new file mode 100644 index 000000000..d4543d645 --- /dev/null +++ b/src/store/reducers/pdisk/utils.ts @@ -0,0 +1,54 @@ +import type {TEvPDiskStateResponse} from '../../../types/api/pdisk'; +import type {TStorageInfo} from '../../../types/api/storage'; +import type {TEvSystemStateResponse} from '../../../types/api/systemState'; +import {preparePDiskData} from '../../../utils/disks/prepareDisks'; +import {prepareNodeSystemState} from '../../../utils/nodes'; +import type {PreparedStorageGroup} from '../storage/types'; +import {prepareStorageGroupData} from '../storage/utils'; + +export function preparePDiksDataResponse([pdiskResponse, nodeResponse]: [ + TEvPDiskStateResponse, + TEvSystemStateResponse, +]) { + const rawPDisk = pdiskResponse.PDiskStateInfo?.[0]; + const preparedPDisk = preparePDiskData(rawPDisk); + + const rawNode = nodeResponse.SystemStateInfo?.[0]; + const preparedNode = prepareNodeSystemState(rawNode); + + return { + ...preparedPDisk, + NodeId: preparedPDisk.NodeId ?? preparedNode.NodeId, + NodeHost: preparedNode.Host, + NodeType: preparedNode.Roles?.[0], + NodeDC: preparedNode.DC, + }; +} + +export function preparePDiskStorageResponse( + data: TStorageInfo, + pDiskId: number | string, + nodeId: number | string, +) { + const preparedGroups: PreparedStorageGroup[] = []; + + data.StoragePools?.forEach((pool) => { + pool.Groups?.filter((group) => { + if (!group?.VDisks) { + return false; + } + + return ( + group.VDisks.filter( + (vdisk) => + (vdisk.PDiskId === pDiskId || vdisk.PDisk?.PDiskId === Number(pDiskId)) && + vdisk.PDisk?.NodeId === Number(nodeId), + ).length > 0 + ); + }).forEach((group) => { + preparedGroups.push(prepareStorageGroupData(group, pool.Name)); + }); + }); + + return preparedGroups; +} diff --git a/src/store/reducers/storage/types.ts b/src/store/reducers/storage/types.ts index 0b105f1b9..486fca395 100644 --- a/src/store/reducers/storage/types.ts +++ b/src/store/reducers/storage/types.ts @@ -73,7 +73,7 @@ export interface StorageSortAndFilterParams extends StorageSortParams { export interface StorageApiRequestParams extends StorageSortAndFilterParams { tenant?: string; - nodeId?: string; + nodeId?: string | number; visibleEntities?: VisibleEntities; version?: EVersion; diff --git a/src/store/reducers/storage/utils.ts b/src/store/reducers/storage/utils.ts index af3079bc3..bfe20d4db 100644 --- a/src/store/reducers/storage/utils.ts +++ b/src/store/reducers/storage/utils.ts @@ -43,7 +43,7 @@ const prepareVDisk = (vDisk: TVDiskStateInfo, poolName: string | undefined) => { }; }; -const prepareStorageGroupData = ( +export const prepareStorageGroupData = ( group: TStorageGroupInfo, poolName?: string, ): PreparedStorageGroup => { @@ -114,7 +114,7 @@ const prepareStorageGroupData = ( }; }; -const prepareStorageGroupDataV2 = (group: TStorageGroupInfoV2): PreparedStorageGroup => { +export const prepareStorageGroupDataV2 = (group: TStorageGroupInfoV2): PreparedStorageGroup => { const { VDisks = [], PoolName, diff --git a/src/utils/disks/constants.ts b/src/utils/disks/constants.ts index 7c4ce9faf..00d06eb62 100644 --- a/src/utils/disks/constants.ts +++ b/src/utils/disks/constants.ts @@ -12,7 +12,15 @@ export const DISK_COLOR_STATE_TO_NUMERIC_SEVERITY: Record = { Red: 5, }; +type SeverityToColor = Record; + +export const DISK_NUMERIC_SEVERITY_TO_STATE_COLOR = Object.entries( + DISK_COLOR_STATE_TO_NUMERIC_SEVERITY, +).reduce((acc, [color, severity]) => ({...acc, [severity]: color as EFlag}), {}); + export const NOT_AVAILABLE_SEVERITY = DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Grey; +export const NOT_AVAILABLE_SEVERITY_COLOR = + DISK_NUMERIC_SEVERITY_TO_STATE_COLOR[NOT_AVAILABLE_SEVERITY]; export const VDISK_STATE_SEVERITY: Record = { [EVDiskState.Initial]: DISK_COLOR_STATE_TO_NUMERIC_SEVERITY.Yellow, diff --git a/src/utils/disks/helpers.ts b/src/utils/disks/helpers.ts index a05e41d52..5441a6b92 100644 --- a/src/utils/disks/helpers.ts +++ b/src/utils/disks/helpers.ts @@ -1,5 +1,14 @@ import type {TVDiskStateInfo, TVSlotId} from '../../types/api/vdisk'; +import {DISK_NUMERIC_SEVERITY_TO_STATE_COLOR, NOT_AVAILABLE_SEVERITY_COLOR} from './constants'; export function isFullVDiskData(disk: TVDiskStateInfo | TVSlotId): disk is TVDiskStateInfo { return 'VDiskId' in disk; } + +export function getSeverityColor(severity: number | undefined) { + if (severity === undefined) { + return NOT_AVAILABLE_SEVERITY_COLOR; + } + + return DISK_NUMERIC_SEVERITY_TO_STATE_COLOR[severity] || NOT_AVAILABLE_SEVERITY_COLOR; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index ffa1fa018..4d231399f 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,3 +1,7 @@ export const getArray = (arrayLength: number) => { return [...Array(arrayLength).keys()]; }; + +export function valueIsDefined(value: T | null | undefined): value is T { + return value !== null && value !== undefined; +} From a99e11afe4270c3ddc8bc6ee78f61284a0a6b1c1 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 15 Mar 2024 16:18:45 +0300 Subject: [PATCH 2/2] fix: review fixes --- src/components/LinkWithIcon/LinkWithIcon.tsx | 5 ++-- src/components/StatusIcon/StatusIcon.tsx | 17 +++++------ src/store/reducers/pdisk/utils.ts | 30 +++++++++----------- 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/components/LinkWithIcon/LinkWithIcon.tsx b/src/components/LinkWithIcon/LinkWithIcon.tsx index a1f5eb2c0..288429a49 100644 --- a/src/components/LinkWithIcon/LinkWithIcon.tsx +++ b/src/components/LinkWithIcon/LinkWithIcon.tsx @@ -2,7 +2,8 @@ import block from 'bem-cn-lite'; import {Link} from '@gravity-ui/uikit'; -import {Icon} from '../Icon/Icon'; +import {ArrowUpRightFromSquare} from '@gravity-ui/icons'; + import {InternalLink} from '../InternalLink'; import './LinkWithIcon.scss'; @@ -19,7 +20,7 @@ export const LinkWithIcon = ({title, url, external = true}: ExternalLinkWithIcon <> {title} {'\u00a0'} - + ); diff --git a/src/components/StatusIcon/StatusIcon.tsx b/src/components/StatusIcon/StatusIcon.tsx index b63636bf4..3e7628cbb 100644 --- a/src/components/StatusIcon/StatusIcon.tsx +++ b/src/components/StatusIcon/StatusIcon.tsx @@ -1,9 +1,10 @@ import {Icon} from '@gravity-ui/uikit'; -import circleExclamationIcon from '../../assets/icons/circle-exclamation.svg'; -import circleInfoIcon from '../../assets/icons/circle-info.svg'; -import circleTimesIcon from '../../assets/icons/circle-xmark.svg'; -import triangleExclamationIcon from '../../assets/icons/triangle-exclamation.svg'; +import CircleExclamationFillIcon from '@gravity-ui/icons/svgs/circle-exclamation-fill.svg'; +import CircleInfoFillIcon from '@gravity-ui/icons/svgs/circle-info-fill.svg'; +import CircleXmarkFillIcon from '@gravity-ui/icons/svgs/circle-xmark-fill.svg'; +import TriangleExclamationFillIcon from '@gravity-ui/icons/svgs/triangle-exclamation-fill.svg'; + import {EFlag} from '../../types/api/enums'; import {cn} from '../../utils/cn'; import './StatusIcon.scss'; @@ -11,10 +12,10 @@ import './StatusIcon.scss'; const b = cn('ydb-status-icon'); const icons = { - [EFlag.Blue]: circleInfoIcon, - [EFlag.Yellow]: circleExclamationIcon, - [EFlag.Orange]: triangleExclamationIcon, - [EFlag.Red]: circleTimesIcon, + [EFlag.Blue]: CircleInfoFillIcon, + [EFlag.Yellow]: CircleExclamationFillIcon, + [EFlag.Orange]: TriangleExclamationFillIcon, + [EFlag.Red]: CircleXmarkFillIcon, }; export type StatusIconMode = 'color' | 'icons'; diff --git a/src/store/reducers/pdisk/utils.ts b/src/store/reducers/pdisk/utils.ts index d4543d645..30d7ea895 100644 --- a/src/store/reducers/pdisk/utils.ts +++ b/src/store/reducers/pdisk/utils.ts @@ -32,23 +32,21 @@ export function preparePDiskStorageResponse( ) { const preparedGroups: PreparedStorageGroup[] = []; - data.StoragePools?.forEach((pool) => { - pool.Groups?.filter((group) => { - if (!group?.VDisks) { - return false; - } + data.StoragePools?.forEach((pool) => + pool.Groups?.forEach((group) => { + const groupHasPDiskVDisks = group.VDisks?.some((vdisk) => { + // If VDisk has PDisk inside, PDiskId and NodeId fields could be only inside PDisk and vice versa + const groupPDiskId = vdisk.PDiskId ?? vdisk.PDisk?.PDiskId; + const groupNodeId = vdisk.NodeId ?? vdisk.PDisk?.NodeId; + + return groupPDiskId === Number(pDiskId) && groupNodeId === Number(nodeId); + }); - return ( - group.VDisks.filter( - (vdisk) => - (vdisk.PDiskId === pDiskId || vdisk.PDisk?.PDiskId === Number(pDiskId)) && - vdisk.PDisk?.NodeId === Number(nodeId), - ).length > 0 - ); - }).forEach((group) => { - preparedGroups.push(prepareStorageGroupData(group, pool.Name)); - }); - }); + if (groupHasPDiskVDisks) { + preparedGroups.push(prepareStorageGroupData(group, pool.Name)); + } + }), + ); return preparedGroups; }