From 8ed47569dde77c7d803de0ae5a7c886f80461054 Mon Sep 17 00:00:00 2001 From: Elena Makarova Date: Fri, 18 Apr 2025 16:55:04 +0300 Subject: [PATCH] feat(Cluster): redesign query dashboard --- src/assets/icons/overview.svg | 1 + src/assets/icons/user-check.svg | 1 - .../DoughnutMetrics/DoughnutMetrics.scss | 21 ++- .../DoughnutMetrics/DoughnutMetrics.tsx | 33 ++-- .../EntityStatusNew/EntityStatus.scss | 10 ++ .../EntityStatusNew/EntityStatus.tsx | 95 +++++++++++ src/components/EntityStatusNew/i18n/en.json | 14 ++ src/components/EntityStatusNew/i18n/index.ts | 7 + src/components/EntityStatusNew/i18n/ru.json | 6 + src/components/EntityStatusNew/utils.ts | 24 +++ src/components/StatusIconNew/StatusIcon.tsx | 31 ++++ src/components/Tag/Tag.scss | 18 --- src/components/Tag/Tag.tsx | 18 --- src/components/Tag/index.ts | 1 - src/components/Tags/Tags.tsx | 11 +- src/containers/Cluster/Cluster.tsx | 36 ++--- .../ClusterDashboard/ClusterDashboard.scss | 52 ------ .../ClusterDashboard/ClusterDashboard.tsx | 146 ----------------- .../components/ClusterMetricsCard.tsx | 73 --------- .../components/ClusterMetricsMemory.tsx | 30 ---- .../components/ClusterMetricsStorage.tsx | 30 ---- .../Cluster/ClusterDashboard/utils.tsx | 97 ----------- .../Cluster/ClusterInfo/ClusterInfo.scss | 8 +- .../Cluster/ClusterInfo/ClusterInfo.tsx | 56 +++---- .../components/NodesState/NodesState.scss | 17 -- .../components/NodesState/NodesState.tsx | 15 -- .../Cluster/ClusterInfo/utils/utils.tsx | 13 +- .../ClusterOverview/ClusterOverview.scss | 65 ++++++++ .../ClusterOverview/ClusterOverview.tsx | 150 ++++++++++++++++++ .../components/ClusterMetricsCard.tsx | 89 +++++++++++ .../components/ClusterMetricsCores.tsx | 31 +++- .../components/ClusterMetricsMemory.tsx | 44 +++++ .../components/ClusterMetricsNetwork.tsx | 52 ++++++ .../components/ClusterMetricsStorage.tsx | 47 ++++++ .../shared.ts | 10 +- .../Cluster/ClusterOverview/utils.tsx | 62 ++++++++ src/containers/Cluster/i18n/en.json | 24 ++- src/containers/Cluster/utils.tsx | 8 +- src/containers/UserSettings/i18n/en.json | 2 + src/containers/UserSettings/settings.tsx | 13 +- src/services/settings.ts | 4 + src/store/reducers/cluster/cluster.ts | 2 +- src/types/api/cluster.ts | 22 ++- src/utils/constants.ts | 4 + 44 files changed, 867 insertions(+), 626 deletions(-) create mode 100644 src/assets/icons/overview.svg delete mode 100644 src/assets/icons/user-check.svg create mode 100644 src/components/EntityStatusNew/EntityStatus.scss create mode 100644 src/components/EntityStatusNew/EntityStatus.tsx create mode 100644 src/components/EntityStatusNew/i18n/en.json create mode 100644 src/components/EntityStatusNew/i18n/index.ts create mode 100644 src/components/EntityStatusNew/i18n/ru.json create mode 100644 src/components/EntityStatusNew/utils.ts create mode 100644 src/components/StatusIconNew/StatusIcon.tsx delete mode 100644 src/components/Tag/Tag.scss delete mode 100644 src/components/Tag/Tag.tsx delete mode 100644 src/components/Tag/index.ts delete mode 100644 src/containers/Cluster/ClusterDashboard/ClusterDashboard.scss delete mode 100644 src/containers/Cluster/ClusterDashboard/ClusterDashboard.tsx delete mode 100644 src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx delete mode 100644 src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx delete mode 100644 src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx delete mode 100644 src/containers/Cluster/ClusterDashboard/utils.tsx delete mode 100644 src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss delete mode 100644 src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.tsx create mode 100644 src/containers/Cluster/ClusterOverview/ClusterOverview.scss create mode 100644 src/containers/Cluster/ClusterOverview/ClusterOverview.tsx create mode 100644 src/containers/Cluster/ClusterOverview/components/ClusterMetricsCard.tsx rename src/containers/Cluster/{ClusterDashboard => ClusterOverview}/components/ClusterMetricsCores.tsx (53%) create mode 100644 src/containers/Cluster/ClusterOverview/components/ClusterMetricsMemory.tsx create mode 100644 src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx create mode 100644 src/containers/Cluster/ClusterOverview/components/ClusterMetricsStorage.tsx rename src/containers/Cluster/{ClusterDashboard => ClusterOverview}/shared.ts (64%) create mode 100644 src/containers/Cluster/ClusterOverview/utils.tsx diff --git a/src/assets/icons/overview.svg b/src/assets/icons/overview.svg new file mode 100644 index 000000000..eac73b134 --- /dev/null +++ b/src/assets/icons/overview.svg @@ -0,0 +1 @@ + diff --git a/src/assets/icons/user-check.svg b/src/assets/icons/user-check.svg deleted file mode 100644 index 042cecb4c..000000000 --- a/src/assets/icons/user-check.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/components/DoughnutMetrics/DoughnutMetrics.scss b/src/components/DoughnutMetrics/DoughnutMetrics.scss index 9087310f7..7b5f0566c 100644 --- a/src/components/DoughnutMetrics/DoughnutMetrics.scss +++ b/src/components/DoughnutMetrics/DoughnutMetrics.scss @@ -1,14 +1,17 @@ .ydb-doughnut-metrics { - --doughnut-border: 11px; + --doughnut-border: 16px; --doughnut-color: var(--ydb-color-status-green); + --doughnut-backdrop-color: var(--g-color-base-positive-light); + --doughnut-overlap-color: var(--g-color-base-positive-heavy-hover); &__doughnut { position: relative; - width: 172px; + width: 100px; aspect-ratio: 1; border-radius: 50%; - background-color: var(--doughnut-color); + + transform: rotate(180deg); &::before { display: block; @@ -25,9 +28,13 @@ } &__doughnut_status_warning { --doughnut-color: var(--ydb-color-status-yellow); + --doughnut-backdrop-color: var(--g-color-base-warning-light); + --doughnut-overlap-color: var(--g-color-base-warning-heavy-hover); } &__doughnut_status_danger { --doughnut-color: var(--ydb-color-status-red); + --doughnut-backdrop-color: var(--g-color-base-danger-light); + --doughnut-overlap-color: var(--g-color-base-danger-heavy-hover); } &__text-wrapper { --wrapper-indent: calc(var(--doughnut-border) + 5px); @@ -44,15 +51,15 @@ width: calc(100% - calc(var(--wrapper-indent) * 2)); text-align: center; + + transform: rotate(180deg); aspect-ratio: 1; } &__value { position: absolute; bottom: 20px; } - &__legend { - height: 50%; - - white-space: pre-wrap; + &__legend-note { + display: flex; } } diff --git a/src/components/DoughnutMetrics/DoughnutMetrics.tsx b/src/components/DoughnutMetrics/DoughnutMetrics.tsx index b596d8936..22d3fc50c 100644 --- a/src/components/DoughnutMetrics/DoughnutMetrics.tsx +++ b/src/components/DoughnutMetrics/DoughnutMetrics.tsx @@ -1,7 +1,7 @@ import React from 'react'; import type {TextProps} from '@gravity-ui/uikit'; -import {Text} from '@gravity-ui/uikit'; +import {Flex, HelpMark, Text} from '@gravity-ui/uikit'; import {cn} from '../../utils/cn'; import type {ProgressStatus} from '../../utils/progress'; @@ -13,18 +13,23 @@ const b = cn('ydb-doughnut-metrics'); interface LegendProps { children?: React.ReactNode; variant?: TextProps['variant']; + color?: TextProps['color']; + note?: React.ReactNode; } -function Legend({children, variant = 'subheader-3'}: LegendProps) { +function Legend({children, variant = 'subheader-3', color = 'primary', note}: LegendProps) { return ( - - {children} - + + + {children} + + {note && {note}} + ); } function Value({children, variant = 'subheader-2'}: LegendProps) { return ( - + {children} ); @@ -38,19 +43,21 @@ interface DoughnutProps { } export function DoughnutMetrics({status, fillWidth, children, className}: DoughnutProps) { - let gradientFill = 'var(--g-color-line-generic-solid)'; - let filledDegrees = fillWidth * 3.6 - 90; + let filledDegrees = fillWidth * 3.6; + let doughnutFillVar = 'var(--doughnut-color)'; + let doughnutBackdropVar = 'var(--doughnut-backdrop-color)'; - if (fillWidth > 50) { - gradientFill = 'var(--doughnut-color)'; - filledDegrees = fillWidth * 3.6 + 90; + if (filledDegrees > 360) { + filledDegrees -= 360; + doughnutBackdropVar = 'var(--doughnut-color)'; + doughnutFillVar = 'var(--doughnut-overlap-color)'; } - const gradientDegrees = filledDegrees; + return (
diff --git a/src/components/EntityStatusNew/EntityStatus.scss b/src/components/EntityStatusNew/EntityStatus.scss new file mode 100644 index 000000000..64b2221c6 --- /dev/null +++ b/src/components/EntityStatusNew/EntityStatus.scss @@ -0,0 +1,10 @@ +.ydb-entity-status-new { + .g-help-mark__button { + color: inherit; + } + + &_orange.g-label { + color: var(--g-color-private-orange-500); + background-color: var(--g-color-private-orange-100); + } +} diff --git a/src/components/EntityStatusNew/EntityStatus.tsx b/src/components/EntityStatusNew/EntityStatus.tsx new file mode 100644 index 000000000..a3875a3d9 --- /dev/null +++ b/src/components/EntityStatusNew/EntityStatus.tsx @@ -0,0 +1,95 @@ +import React from 'react'; + +import type {LabelProps} from '@gravity-ui/uikit'; +import {ActionTooltip, Flex, HelpMark, Label} from '@gravity-ui/uikit'; + +import {EFlag} from '../../types/api/enums'; +import {cn} from '../../utils/cn'; +import {StatusIcon} from '../StatusIconNew/StatusIcon'; + +import i18n from './i18n'; +import {EFlagToDescription} from './utils'; + +import './EntityStatus.scss'; + +const b = cn('ydb-entity-status-new'); + +const EFlagToLabelTheme: Record = { + [EFlag.Red]: 'danger', + [EFlag.Blue]: 'info', + [EFlag.Green]: 'success', + [EFlag.Grey]: 'unknown', + [EFlag.Orange]: 'orange', + [EFlag.Yellow]: 'warning', +}; + +const EFlagToStatusName: Record = { + get [EFlag.Red]() { + return i18n('title_red'); + }, + get [EFlag.Yellow]() { + return i18n('title_yellow'); + }, + get [EFlag.Orange]() { + return i18n('title_orange'); + }, + get [EFlag.Green]() { + return i18n('title_green'); + }, + get [EFlag.Grey]() { + return i18n('title_grey'); + }, + get [EFlag.Blue]() { + return i18n('title_blue'); + }, +}; + +interface EntityStatusLabelProps { + status: EFlag; + note?: React.ReactNode; + children?: React.ReactNode; + withStatusName?: boolean; + size?: LabelProps['size']; +} + +function EntityStatusLabel({ + children, + status, + withStatusName = true, + note, + size = 'm', +}: EntityStatusLabelProps) { + const theme = EFlagToLabelTheme[status]; + return ( + + + + ); +} + +interface EntityStatusProps { + children?: React.ReactNode; + className?: string; +} + +export function EntityStatus({className, children}: EntityStatusProps) { + return ( + + {children} + + ); +} + +EntityStatus.Label = EntityStatusLabel; +EntityStatus.displayName = 'EntityStatus'; diff --git a/src/components/EntityStatusNew/i18n/en.json b/src/components/EntityStatusNew/i18n/en.json new file mode 100644 index 000000000..896df0ec3 --- /dev/null +++ b/src/components/EntityStatusNew/i18n/en.json @@ -0,0 +1,14 @@ +{ + "title_red": "Failed", + "title_blue": "Normal", + "title_green": "Good", + "title_grey": "Unknown", + "title_orange": "Caution", + "title_yellow": "Warning", + "context_red": "Some systems are failed and not available", + "context_yellow": "There are minor issues", + "context_orange": "Critical state, requires immediate attention", + "context_green": "Everything is working as expected", + "context_grey": "The condition cannot be determined", + "context_blue": "All good, some parts of the system are restoring" +} diff --git a/src/components/EntityStatusNew/i18n/index.ts b/src/components/EntityStatusNew/i18n/index.ts new file mode 100644 index 000000000..176f01344 --- /dev/null +++ b/src/components/EntityStatusNew/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-entity-status'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/components/EntityStatusNew/i18n/ru.json b/src/components/EntityStatusNew/i18n/ru.json new file mode 100644 index 000000000..2e23be569 --- /dev/null +++ b/src/components/EntityStatusNew/i18n/ru.json @@ -0,0 +1,6 @@ +{ + "403.title": "Доступ запрещен", + "403.description": "У вас недостаточно прав для просмотра данной страницы.", + "responseError.defaultMessage": "Ошибка запроса", + "error.title": "Ошибка" +} diff --git a/src/components/EntityStatusNew/utils.ts b/src/components/EntityStatusNew/utils.ts new file mode 100644 index 000000000..16a896fdf --- /dev/null +++ b/src/components/EntityStatusNew/utils.ts @@ -0,0 +1,24 @@ +import {EFlag} from '../../types/api/enums'; + +import i18n from './i18n'; + +export const EFlagToDescription: Record = { + get [EFlag.Red]() { + return i18n('context_red'); + }, + get [EFlag.Yellow]() { + return i18n('context_yellow'); + }, + get [EFlag.Orange]() { + return i18n('context_orange'); + }, + get [EFlag.Green]() { + return i18n('context_green'); + }, + get [EFlag.Grey]() { + return i18n('context_grey'); + }, + get [EFlag.Blue]() { + return i18n('context_blue'); + }, +}; diff --git a/src/components/StatusIconNew/StatusIcon.tsx b/src/components/StatusIconNew/StatusIcon.tsx new file mode 100644 index 000000000..7808c7edc --- /dev/null +++ b/src/components/StatusIconNew/StatusIcon.tsx @@ -0,0 +1,31 @@ +import { + CircleCheck, + CircleExclamation, + CircleInfo, + PlugConnection, + TriangleExclamation, +} from '@gravity-ui/icons'; +import type {IconProps} from '@gravity-ui/uikit'; +import {Icon} from '@gravity-ui/uikit'; + +import {EFlag} from '../../types/api/enums'; + +const EFlagToIcon: Record) => React.JSX.Element> = { + [EFlag.Blue]: CircleInfo, + [EFlag.Yellow]: CircleExclamation, + [EFlag.Orange]: TriangleExclamation, + [EFlag.Red]: CircleExclamation, + [EFlag.Green]: CircleCheck, + [EFlag.Grey]: PlugConnection, +}; + +interface StatusIconProps extends Omit { + status?: EFlag; +} + +export function StatusIcon({status, ...props}: StatusIconProps) { + if (!status) { + return null; + } + return ; +} diff --git a/src/components/Tag/Tag.scss b/src/components/Tag/Tag.scss deleted file mode 100644 index 04d131492..000000000 --- a/src/components/Tag/Tag.scss +++ /dev/null @@ -1,18 +0,0 @@ -.tag { - padding: 2px 5px; - - font-size: 12px; - white-space: nowrap; - - color: var(--g-color-text-primary); - border-radius: 3px; - background: var(--g-color-base-generic); - - &:last-child { - margin-right: 0; - } - - &_type_blue { - background-color: var(--g-color-celestial-thunder); - } -} diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx deleted file mode 100644 index bd2806554..000000000 --- a/src/components/Tag/Tag.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; - -import {cn} from '../../utils/cn'; - -import './Tag.scss'; - -const b = cn('tag'); - -export type TagType = 'blue'; - -interface TagProps { - text: React.ReactNode; - type?: TagType; -} - -export const Tag = ({text, type}: TagProps) => { - return
{text}
; -}; diff --git a/src/components/Tag/index.ts b/src/components/Tag/index.ts deleted file mode 100644 index 9790fcbf1..000000000 --- a/src/components/Tag/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './Tag'; diff --git a/src/components/Tags/Tags.tsx b/src/components/Tags/Tags.tsx index 3030b45f3..7e02aca9d 100644 --- a/src/components/Tags/Tags.tsx +++ b/src/components/Tags/Tags.tsx @@ -1,23 +1,18 @@ import React from 'react'; import type {FlexProps} from '@gravity-ui/uikit'; -import {Flex} from '@gravity-ui/uikit'; - -import type {TagType} from '../Tag'; -import {Tag} from '../Tag'; +import {Flex, Label} from '@gravity-ui/uikit'; interface TagsProps { tags: React.ReactNode[]; - tagsType?: TagType; className?: string; gap?: FlexProps['gap']; } -export const Tags = ({tags, tagsType, className = '', gap = 1}: TagsProps) => { +export const Tags = ({tags, className = '', gap = 1}: TagsProps) => { return ( - {tags && - tags.map((tag, tagIndex) => )} + {tags && tags.map((tag, tagIndex) => )} ); }; diff --git a/src/containers/Cluster/Cluster.tsx b/src/containers/Cluster/Cluster.tsx index 892593ebc..2bfc51b49 100644 --- a/src/containers/Cluster/Cluster.tsx +++ b/src/containers/Cluster/Cluster.tsx @@ -6,7 +6,8 @@ import {Redirect, Route, Switch, useRouteMatch} from 'react-router-dom'; import {StringParam, useQueryParams} from 'use-query-params'; import {AutoRefreshControl} from '../../components/AutoRefreshControl/AutoRefreshControl'; -import {EntityStatus} from '../../components/EntityStatus/EntityStatus'; +import {EntityStatus} from '../../components/EntityStatusNew/EntityStatus'; +import {EFlagToDescription} from '../../components/EntityStatusNew/utils'; import {InternalLink} from '../../components/InternalLink'; import routes, {getLocationObjectFromHref} from '../../routes'; import {useClusterDashboardAvailable} from '../../store/reducers/capabilities/hooks'; @@ -23,6 +24,7 @@ import type { AdditionalNodesProps, AdditionalTenantsProps, } from '../../types/additionalProps'; +import {EFlag} from '../../types/api/enums'; import {cn} from '../../utils/cn'; import {useTypedDispatch, useTypedSelector} from '../../utils/hooks'; import {Nodes} from '../Nodes/Nodes'; @@ -31,8 +33,7 @@ import {TabletsTable} from '../Tablets/TabletsTable'; import {Tenants} from '../Tenants/Tenants'; import {VersionsContainer} from '../Versions/Versions'; -import {ClusterDashboard} from './ClusterDashboard/ClusterDashboard'; -import {ClusterInfo} from './ClusterInfo/ClusterInfo'; +import {ClusterOverview} from './ClusterOverview/ClusterOverview'; import type {ClusterTab} from './utils'; import {clusterTabs, clusterTabsIds, getClusterPath, isClusterTab} from './utils'; @@ -91,14 +92,16 @@ export function Cluster({ if (infoLoading) { return ; } + const clusterStatus = cluster?.Overall || EFlag.Grey; return ( - + + {clusterTitle} + + ); }; @@ -120,11 +123,12 @@ export function Cluster({
{isClusterDashboardAvailable && ( - )}
@@ -150,18 +154,6 @@ export function Cluster({ />
- - - - {formatNumber(value)} - - ); -} - -interface ClusterDashboardProps { - cluster: TClusterInfo; - groupStats?: ClusterGroupsStats; - loading?: boolean; - error?: IResponseError | string; -} - -export function ClusterDashboard({cluster, ...props}: ClusterDashboardProps) { - if (props.error) { - return ; - } - return ( -
- - - - -
- -
-
-
- ); -} - -function ClusterDoughnuts({cluster, loading}: ClusterDashboardProps) { - if (loading) { - return ; - } - const metricsCards = []; - if (isClusterInfoV2(cluster)) { - const {CoresUsed, NumberOfCpus, CoresTotal} = cluster; - const total = CoresTotal ?? NumberOfCpus; - if (valueIsDefined(CoresUsed) && valueIsDefined(total)) { - metricsCards.push( - , - ); - } - } - const {StorageTotal, StorageUsed} = cluster; - if (valueIsDefined(StorageTotal) && valueIsDefined(StorageUsed)) { - metricsCards.push( - , - ); - } - const {MemoryTotal, MemoryUsed} = cluster; - if (valueIsDefined(MemoryTotal) && valueIsDefined(MemoryUsed)) { - metricsCards.push( - , - ); - } - return metricsCards; -} - -function ClusterDashboardCards({cluster, groupStats = {}, loading}: ClusterDashboardProps) { - if (loading) { - return null; - } - const cards = []; - - const nodesRoles = getNodesRolesInfo(cluster); - cards.push( - - - - - {nodesRoles?.length ? : null} - - , - ); - - if (Object.keys(groupStats).length) { - const tags = getStorageGroupStats(groupStats); - const total = getTotalStorageGroupsUsed(groupStats); - cards.push( - - - - - - , - ); - } - - const dataCenters = getDCInfo(cluster); - if (dataCenters?.length) { - cards.push( - - - - - - , - ); - } - - if (cluster.Tenants) { - cards.push( - - - , - ); - } - return cards; -} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx deleted file mode 100644 index 60fe1bd6b..000000000 --- a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCard.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import React from 'react'; - -import {Text} from '@gravity-ui/uikit'; - -import type {DiagnosticCardProps} from '../../../../components/DiagnosticCard/DiagnosticCard'; -import {DiagnosticCard} from '../../../../components/DiagnosticCard/DiagnosticCard'; -import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; -import {Skeleton} from '../../../../components/Skeleton/Skeleton'; -import type {ProgressStatus} from '../../../../utils/progress'; -import {b} from '../shared'; - -interface ClusterMetricsDougnutCardProps extends ClusterMetricsCommonCardProps { - status: ProgressStatus; - fillWidth: number; -} - -interface ClusterMetricsCommonCardProps { - children?: React.ReactNode; - title?: string; - size?: DiagnosticCardProps['size']; - className?: string; -} - -export function ClusterMetricsCard({ - children, - title, - size, - className, -}: ClusterMetricsCommonCardProps) { - return ( - - {title ? ( - - {title} - - ) : null} - {children} - - ); -} - -export function ClusterMetricsCardDoughnut({ - title, - children, - size, - ...rest -}: ClusterMetricsDougnutCardProps) { - return ( - - - {children} - - - ); -} - -function ClusterMetricsCardSkeleton() { - return ( - - - - ); -} - -export function ClusterDashboardSkeleton() { - return ( - - - - - - ); -} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx deleted file mode 100644 index 7959395bd..000000000 --- a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsMemory.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; -import {formatStorageValues} from '../../../../utils/dataFormatters/dataFormatters'; -import i18n from '../../i18n'; -import type {ClusterMetricsCommonProps} from '../shared'; -import {useDiagramValues} from '../utils'; - -import {ClusterMetricsCardDoughnut} from './ClusterMetricsCard'; - -interface ClusterMetricsMemoryProps extends ClusterMetricsCommonProps {} - -function formatStorageLegend({value, capacity}: {value: number; capacity: number}) { - const formatted = formatStorageValues(value, capacity, undefined, '\n'); - return `${formatted[0]} / ${formatted[1]}`; -} - -export function ClusterMetricsMemory({value, capacity, ...rest}: ClusterMetricsMemoryProps) { - const {status, percents, legend, fill} = useDiagramValues({ - value, - capacity, - legendFormatter: formatStorageLegend, - ...rest, - }); - - return ( - - {legend} - {percents} - - ); -} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx b/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx deleted file mode 100644 index 07cb6e539..000000000 --- a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsStorage.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; -import {formatStorageValues} from '../../../../utils/dataFormatters/dataFormatters'; -import i18n from '../../i18n'; -import type {ClusterMetricsCommonProps} from '../shared'; -import {useDiagramValues} from '../utils'; - -import {ClusterMetricsCardDoughnut} from './ClusterMetricsCard'; - -interface ClusterMetricsStorageProps extends ClusterMetricsCommonProps {} - -function formatStorageLegend({value, capacity}: {value: number; capacity: number}) { - const formatted = formatStorageValues(value, capacity, undefined, '\n'); - return `${formatted[0]} / ${formatted[1]}`; -} - -export function ClusterMetricsStorage({value, capacity, ...rest}: ClusterMetricsStorageProps) { - const {status, percents, legend, fill} = useDiagramValues({ - value, - capacity, - legendFormatter: formatStorageLegend, - ...rest, - }); - - return ( - - {legend} - {percents} - - ); -} diff --git a/src/containers/Cluster/ClusterDashboard/utils.tsx b/src/containers/Cluster/ClusterDashboard/utils.tsx deleted file mode 100644 index 78de7fc0a..000000000 --- a/src/containers/Cluster/ClusterDashboard/utils.tsx +++ /dev/null @@ -1,97 +0,0 @@ -import React from 'react'; - -import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; -import {isClusterInfoV2} from '../../../types/api/cluster'; -import type {TClusterInfo} from '../../../types/api/cluster'; -import {formatNumber, formatPercent} from '../../../utils/dataFormatters/dataFormatters'; -import {calculateProgressStatus} from '../../../utils/progress'; -import {DiskGroupsErasureStats} from '../ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars'; - -import type {ClusterMetricsCommonProps} from './shared'; - -export function useDiagramValues({ - value, - capacity, - colorizeProgress = true, - warningThreshold, - dangerThreshold, - inverseColorize = false, - legendFormatter, -}: ClusterMetricsCommonProps & { - legendFormatter: (params: {value: number; capacity: number}) => string; -}) { - const parsedValue = parseFloat(String(value)); - const parsedCapacity = parseFloat(String(capacity)); - let fillWidth = (parsedValue / parsedCapacity) * 100 || 0; - fillWidth = fillWidth > 100 ? 100 : fillWidth; - const normalizedFillWidth = fillWidth < 1 ? 0.5 : fillWidth; - const status = calculateProgressStatus({ - fillWidth, - warningThreshold, - dangerThreshold, - colorizeProgress, - inverseColorize, - }); - - const percents = formatPercent(fillWidth / 100); - const legend = legendFormatter({ - value: parsedValue, - capacity: parsedCapacity, - }); - - return {status, percents, legend, fill: normalizedFillWidth}; -} - -export function getDCInfo(cluster: TClusterInfo) { - if (isClusterInfoV2(cluster) && cluster.MapDataCenters) { - return Object.keys(cluster.MapDataCenters); - } - return cluster.DataCenters?.filter(Boolean); -} - -const rolesToShow = ['storage', 'tenant']; - -export function getNodesRolesInfo(cluster: TClusterInfo) { - const nodesRoles: React.ReactNode[] = []; - if (isClusterInfoV2(cluster) && cluster.MapNodeRoles) { - for (const [role, count] of Object.entries(cluster.MapNodeRoles)) { - if (rolesToShow.includes(role.toLowerCase())) { - nodesRoles.push( - - {role}: {formatNumber(count)} - , - ); - } - } - } - return nodesRoles; -} - -export function getStorageGroupStats(groupStats: ClusterGroupsStats) { - const result: React.ReactNode[] = []; - - Object.entries(groupStats).forEach(([storageType, stats]) => { - Object.values(stats).forEach((erasureStats) => { - result.push( - - {storageType}: {formatNumber(erasureStats.createdGroups)} /{' '} - {formatNumber(erasureStats.totalGroups)} - , - ); - }); - }); - return result; -} - -export const getTotalStorageGroupsUsed = (groupStats: ClusterGroupsStats) => { - return Object.values(groupStats).reduce((acc, data) => { - Object.values(data).forEach((erasureStats) => { - acc += erasureStats.createdGroups; - }); - - return acc; - }, 0); -}; diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss index 3e0555f10..e61320671 100644 --- a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss +++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss @@ -1,7 +1,8 @@ @use '../../../styles/mixins'; .cluster-info { - padding: 20px 0; + --g-definition-list-item-gap: var(--g-spacing-2); + padding: var(--g-spacing-5) 0 var(--g-spacing-2); @include mixins.body-2-typography(); @@ -9,11 +10,6 @@ margin-top: 5px; } - &__section-title { - margin: var(--g-spacing-1) 0 var(--g-spacing-3); - @include mixins.text-subheader-2(); - } - &__dc { height: 20px; } diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx index 13cff38e1..3033a450e 100644 --- a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx +++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx @@ -17,7 +17,7 @@ import './ClusterInfo.scss'; interface ClusterInfoProps { cluster?: TClusterInfo; loading?: boolean; - error?: IResponseError; + error?: IResponseError | string; additionalClusterProps?: AdditionalClusterProps; } @@ -40,49 +40,33 @@ export const ClusterInfo = ({ } return ( -
-
{i18n('title_info')}
- - {clusterInfo.map(({label, value}) => { - return ( - - {value} - - ); - })} - -
+ + {clusterInfo.map(({label, value}) => { + return ( + + {value} + + ); + })} + {linksList.length > 0 && ( + + + {linksList.map(({title, url}) => { + return ; + })} + + + )} + ); }; - const renderLinks = () => { - if (linksList.length) { - return ( -
-
{i18n('title_links')}
- - {linksList.map(({title, url}) => { - return ; - })} - -
- ); - } - - return null; - }; - const renderContent = () => { if (loading) { return ; } - return ( - - {renderInfo()} - {renderLinks()} - - ); + return renderInfo(); }; return ( diff --git a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss deleted file mode 100644 index 7db889550..000000000 --- a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss +++ /dev/null @@ -1,17 +0,0 @@ -@use '../../../../../styles/mixins.scss'; - -.ydb-nodes-state { - display: flex; - justify-content: center; - align-items: center; - - width: max-content; - min-width: 26px; - height: 20px; - padding: 0 var(--g-spacing-1); - - color: var(--entity-state-font-color); - border-radius: var(--g-spacing-1); - background-color: var(--entity-state-background-color); - @include mixins.entity-state-colors(); -} diff --git a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.tsx b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.tsx deleted file mode 100644 index bd1557e0c..000000000 --- a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type {EFlag} from '../../../../../types/api/enums'; -import {cn} from '../../../../../utils/cn'; - -import './NodesState.scss'; - -const b = cn('ydb-nodes-state'); - -interface NodesStateProps { - state: EFlag; - children: React.ReactNode; -} - -export function NodesState({state, children}: NodesStateProps) { - return
{children}
; -} diff --git a/src/containers/Cluster/ClusterInfo/utils/utils.tsx b/src/containers/Cluster/ClusterInfo/utils/utils.tsx index e51934a51..5771a4e7a 100644 --- a/src/containers/Cluster/ClusterInfo/utils/utils.tsx +++ b/src/containers/Cluster/ClusterInfo/utils/utils.tsx @@ -2,6 +2,7 @@ import React from 'react'; import {Flex} from '@gravity-ui/uikit'; +import {EntityStatus} from '../../../../components/EntityStatusNew/EntityStatus'; import {ProgressViewer} from '../../../../components/ProgressViewer/ProgressViewer'; import {Tags} from '../../../../components/Tags'; import {isClusterInfoV2} from '../../../../types/api/cluster'; @@ -10,7 +11,6 @@ import type {EFlag} from '../../../../types/api/enums'; import type {InfoItem} from '../../../../types/components'; import {formatNumber} from '../../../../utils/dataFormatters/dataFormatters'; import i18n from '../../i18n'; -import {NodesState} from '../components/NodesState/NodesState'; import {b} from '../shared'; const COLORS_PRIORITY: Record = { @@ -26,7 +26,7 @@ const getDCInfo = (cluster: TClusterInfo) => { if (isClusterInfoV2(cluster) && cluster.MapDataCenters) { return Object.entries(cluster.MapDataCenters).map(([dc, count]) => ( - {dc}: {formatNumber(count)} + {dc} : {formatNumber(count)} )); } @@ -42,9 +42,14 @@ export const getInfo = (cluster: TClusterInfo, additionalInfo: InfoItem[]) => { arrayNodesStates.sort((a, b) => COLORS_PRIORITY[b[0]] - COLORS_PRIORITY[a[0]]); const nodesStates = arrayNodesStates.map(([state, count]) => { return ( - + {formatNumber(count)} - + ); }); info.push({ diff --git a/src/containers/Cluster/ClusterOverview/ClusterOverview.scss b/src/containers/Cluster/ClusterOverview/ClusterOverview.scss new file mode 100644 index 000000000..14d648344 --- /dev/null +++ b/src/containers/Cluster/ClusterOverview/ClusterOverview.scss @@ -0,0 +1,65 @@ +@use '../../../styles/mixins.scss'; + +.ydb-cluster-dashboard { + &__dashboard-wrapper { + gap: var(--g-spacing-4); + + padding-top: var(--g-spacing-4); + } + + &__dashboard-wrapper_collapsed { + gap: var(--g-spacing-2); + + margin-right: var(--g-spacing-4); + margin-left: auto; + padding-top: unset; + } + + &__error { + @include mixins.body-2-typography(); + } + + &__skeleton-wrapper { + padding: unset; + + border: unset; + } + &__skeleton { + height: 100%; + } + &__overview-wrapper { + --g-button-background-color-hover: var(--g-color-base-background); + --g-button-padding: 0; + position: sticky; + left: 0; + + padding: var(--g-spacing-4); + + border: 1px solid var(--g-color-line-generic); + border-radius: 5px; + + .g-button:active { + transform: unset; + } + } + + &__overview-wrapper_collapsed { + &:hover { + border-color: var(--g-color-line-generic-hover); + } + } + + &__disclosure-summary { + display: flex; + justify-content: space-between; + align-items: center; + + width: 100%; + height: 28px; + + cursor: pointer; + .g-button__text { + width: 100%; + } + } +} diff --git a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx new file mode 100644 index 000000000..00ad98d81 --- /dev/null +++ b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx @@ -0,0 +1,150 @@ +import {ArrowToggle, Disclosure, Flex, Icon, Text} from '@gravity-ui/uikit'; + +import {ResponseError} from '../../../components/Errors/ResponseError'; +import {useClusterDashboardAvailable} from '../../../store/reducers/capabilities/hooks'; +import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; +import type {AdditionalClusterProps} from '../../../types/additionalProps'; +import {isClusterInfoV2, isClusterInfoV5} from '../../../types/api/cluster'; +import type {TClusterInfo} from '../../../types/api/cluster'; +import type {IResponseError} from '../../../types/api/error'; +import {valueIsDefined} from '../../../utils'; +import {EXPAND_CLUSTER_DASHBOARD} from '../../../utils/constants'; +import {useSetting} from '../../../utils/hooks/useSetting'; +import {ClusterInfo} from '../ClusterInfo/ClusterInfo'; +import i18n from '../i18n'; + +import {ClusterDashboardSkeleton} from './components/ClusterMetricsCard'; +import {ClusterMetricsCores} from './components/ClusterMetricsCores'; +import {ClusterMetricsMemory} from './components/ClusterMetricsMemory'; +import {ClusterMetricsNetwork} from './components/ClusterMetricsNetwork'; +import {ClusterMetricsStorage} from './components/ClusterMetricsStorage'; +import {b} from './shared'; +import {getTotalStorageGroupsUsed} from './utils'; + +import overviewIcon from '../../../assets/icons/overview.svg'; + +import './ClusterOverview.scss'; + +interface ClusterOverviewProps { + cluster: TClusterInfo; + groupStats?: ClusterGroupsStats; + loading?: boolean; + error?: IResponseError | string; + additionalClusterProps?: AdditionalClusterProps; + collapsed?: boolean; +} + +export function ClusterOverview(props: ClusterOverviewProps) { + const [expandDashboard, setExpandDashboard] = useSetting(EXPAND_CLUSTER_DASHBOARD); + if (props.error) { + return ; + } + + return ( + + setExpandDashboard(!expandDashboard)} + > + + {(disclosureProps) => ( +
+ + + + + {i18n('label_overview')} + + + {!expandDashboard && } + + +
+ )} +
+ + +
+
+ ); +} + +interface ClusterDashboardProps extends ClusterOverviewProps { + collapsed?: boolean; +} + +function ClusterDashboard({collapsed, ...props}: ClusterDashboardProps) { + const isClusterDashboardAvailable = useClusterDashboardAvailable(); + if (!isClusterDashboardAvailable) { + return null; + } + return ( + + + + ); +} + +function ClusterDoughnuts({cluster, groupStats = {}, loading, collapsed}: ClusterOverviewProps) { + if (loading) { + return ; + } + const metricsCards = []; + if (isClusterInfoV2(cluster)) { + const {CoresUsed, NumberOfCpus, CoresTotal} = cluster; + const total = CoresTotal ?? NumberOfCpus; + if (valueIsDefined(CoresUsed) && valueIsDefined(total)) { + metricsCards.push( + , + ); + } + } + const {StorageTotal, StorageUsed} = cluster; + if (valueIsDefined(StorageTotal) && valueIsDefined(StorageUsed)) { + const total = getTotalStorageGroupsUsed(groupStats); + metricsCards.push( + , + ); + } + const {MemoryTotal, MemoryUsed} = cluster; + if (valueIsDefined(MemoryTotal) && valueIsDefined(MemoryUsed)) { + metricsCards.push( + , + ); + } + if (isClusterInfoV5(cluster)) { + const {NetworkUtilization, NetworkWriteThroughput} = cluster; + if (valueIsDefined(NetworkUtilization)) { + metricsCards.push( + , + ); + } + } + + return metricsCards; +} diff --git a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsCard.tsx b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsCard.tsx new file mode 100644 index 000000000..430265d9b --- /dev/null +++ b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsCard.tsx @@ -0,0 +1,89 @@ +import React from 'react'; + +import {Flex} from '@gravity-ui/uikit'; + +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {EntityStatus} from '../../../../components/EntityStatusNew/EntityStatus'; +import {Skeleton} from '../../../../components/Skeleton/Skeleton'; +import {EFlag} from '../../../../types/api/enums'; +import type {ProgressStatus} from '../../../../utils/progress'; +import {b} from '../shared'; + +const ProgressStatusToEFlag: Record = { + good: EFlag.Green, + warning: EFlag.Yellow, + danger: EFlag.Red, +}; + +interface ClusterMetricsDougnutCardProps extends ClusterMetricsCommonCardProps { + status: ProgressStatus; + fillWidth: number; + legend: {main?: string; secondary?: string; note?: React.ReactNode}; +} + +interface ClusterMetricsCommonCardProps { + children?: React.ReactNode; + title?: string; + className?: string; + collapsed?: boolean; +} + +export function ClusterMetricsCard({children, className}: ClusterMetricsCommonCardProps) { + return ( + + {children} + + ); +} + +export function ClusterMetricsCardContent({ + title, + children, + legend, + collapsed, + ...rest +}: ClusterMetricsDougnutCardProps) { + const {main: mainLegend, secondary: secondaryLegend, note: legendNote} = legend; + + if (collapsed) { + const {status, fillWidth} = rest; + const normalizedFillWidth = fillWidth.toFixed(fillWidth > 0 ? 0 : 1); + + return ( + + {`${title} : ${normalizedFillWidth}%`} + + ); + } + return ( + + {children} +
+ {mainLegend && {mainLegend}} + {secondaryLegend && ( + + {secondaryLegend} + + )} +
+
+ ); +} + +function ClusterMetricsCardSkeleton() { + return ( + + + + ); +} + +export function ClusterDashboardSkeleton() { + return ( + + + + + + ); +} diff --git a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCores.tsx b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsCores.tsx similarity index 53% rename from src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCores.tsx rename to src/containers/Cluster/ClusterOverview/components/ClusterMetricsCores.tsx index fc87918ea..571c1a1dc 100644 --- a/src/containers/Cluster/ClusterDashboard/components/ClusterMetricsCores.tsx +++ b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsCores.tsx @@ -2,9 +2,9 @@ import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMe import {formatNumber, formatNumericValues} from '../../../../utils/dataFormatters/dataFormatters'; import i18n from '../../i18n'; import type {ClusterMetricsCommonProps} from '../shared'; -import {useDiagramValues} from '../utils'; +import {getDiagramValues} from '../utils'; -import {ClusterMetricsCardDoughnut} from './ClusterMetricsCard'; +import {ClusterMetricsCardContent} from './ClusterMetricsCard'; interface ClusterMetricsCoresProps extends ClusterMetricsCommonProps {} @@ -15,20 +15,35 @@ function formatCoresLegend({value, capacity}: {value: number; capacity: number}) } else { formatted = formatNumericValues(value, capacity, undefined, '', true); } - return `${formatted[0]} / ${formatted[1]}\n${i18n('context_cores')}`; + return `${formatted[0]} ${i18n('context_of')} ${formatted[1]} ${i18n('context_cores')}`; } -export function ClusterMetricsCores({value, capacity, ...rest}: ClusterMetricsCoresProps) { - const {status, percents, legend, fill} = useDiagramValues({ +export function ClusterMetricsCores({ + collapsed, + value, + capacity, + ...rest +}: ClusterMetricsCoresProps) { + const {status, percents, legend, fill} = getDiagramValues({ value, capacity, legendFormatter: formatCoresLegend, ...rest, }); + return ( - - {legend} + {percents} - + ); } diff --git a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsMemory.tsx b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsMemory.tsx new file mode 100644 index 000000000..12eb10975 --- /dev/null +++ b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsMemory.tsx @@ -0,0 +1,44 @@ +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {formatStorageValues} from '../../../../utils/dataFormatters/dataFormatters'; +import i18n from '../../i18n'; +import type {ClusterMetricsCommonProps} from '../shared'; +import {getDiagramValues} from '../utils'; + +import {ClusterMetricsCardContent} from './ClusterMetricsCard'; + +interface ClusterMetricsMemoryProps extends ClusterMetricsCommonProps {} + +function formatStorageLegend({value, capacity}: {value: number; capacity: number}) { + const formatted = formatStorageValues(value, capacity, undefined, '\n'); + return `${formatted[0]} ${i18n('context_of')} ${formatted[1]}`; +} + +export function ClusterMetricsMemory({ + value, + capacity, + collapsed, + ...rest +}: ClusterMetricsMemoryProps) { + const {status, percents, legend, fill} = getDiagramValues({ + value, + capacity, + legendFormatter: formatStorageLegend, + ...rest, + }); + + return ( + + {percents} + + ); +} diff --git a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx new file mode 100644 index 000000000..2ac9d968c --- /dev/null +++ b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsNetwork.tsx @@ -0,0 +1,52 @@ +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {formatBytes} from '../../../../utils/bytesParsers'; +import {SHOW_NETWORK_UTILIZATION} from '../../../../utils/constants'; +import {useSetting} from '../../../../utils/hooks/useSetting'; +import i18n from '../../i18n'; +import type {ClusterMetricsBaseProps} from '../shared'; +import {calculateBaseDiagramValues} from '../utils'; + +import {ClusterMetricsCardContent} from './ClusterMetricsCard'; + +interface ClusterMetricsNetworkProps extends ClusterMetricsBaseProps { + percentsValue: number; + throughput?: string; +} + +function formatStorageLegend(value?: string) { + return formatBytes({value, withSpeedLabel: true}); +} + +export function ClusterMetricsNetwork({ + percentsValue, + throughput, + collapsed, + ...rest +}: ClusterMetricsNetworkProps) { + const [showNetworkUtilization] = useSetting(SHOW_NETWORK_UTILIZATION); + if (!showNetworkUtilization) { + return null; + } + const {status, percents, fill} = calculateBaseDiagramValues({ + fillWidth: percentsValue * 100, + ...rest, + }); + + const legend = formatStorageLegend(throughput); + + return ( + + {percents} + + ); +} diff --git a/src/containers/Cluster/ClusterOverview/components/ClusterMetricsStorage.tsx b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsStorage.tsx new file mode 100644 index 000000000..e5d8c9011 --- /dev/null +++ b/src/containers/Cluster/ClusterOverview/components/ClusterMetricsStorage.tsx @@ -0,0 +1,47 @@ +import {DoughnutMetrics} from '../../../../components/DoughnutMetrics/DoughnutMetrics'; +import {formatStorageValues} from '../../../../utils/dataFormatters/dataFormatters'; +import i18n from '../../i18n'; +import type {ClusterMetricsCommonProps} from '../shared'; +import {getDiagramValues} from '../utils'; + +import {ClusterMetricsCardContent} from './ClusterMetricsCard'; + +interface ClusterMetricsStorageProps extends ClusterMetricsCommonProps { + groups: number; +} + +function formatStorageLegend({value, capacity}: {value: number; capacity: number}) { + const formatted = formatStorageValues(value, capacity, undefined, '\n'); + return `${formatted[0]} ${i18n('context_of')} ${formatted[1]}`; +} + +export function ClusterMetricsStorage({ + value, + capacity, + groups, + collapsed, + ...rest +}: ClusterMetricsStorageProps) { + const {status, percents, legend, fill} = getDiagramValues({ + value, + capacity, + legendFormatter: formatStorageLegend, + ...rest, + }); + + return ( + + {percents} + + ); +} diff --git a/src/containers/Cluster/ClusterDashboard/shared.ts b/src/containers/Cluster/ClusterOverview/shared.ts similarity index 64% rename from src/containers/Cluster/ClusterDashboard/shared.ts rename to src/containers/Cluster/ClusterOverview/shared.ts index 23591c8fd..ae3ef09e0 100644 --- a/src/containers/Cluster/ClusterDashboard/shared.ts +++ b/src/containers/Cluster/ClusterOverview/shared.ts @@ -1,11 +1,15 @@ import {cn} from '../../../utils/cn'; export const b = cn('ydb-cluster-dashboard'); -export interface ClusterMetricsCommonProps { - value: number | string; - capacity: number | string; +export interface ClusterMetricsBaseProps { colorizeProgress?: boolean; inverseColorize?: boolean; warningThreshold?: number; dangerThreshold?: number; + collapsed?: boolean; +} + +export interface ClusterMetricsCommonProps extends ClusterMetricsBaseProps { + value: number | string; + capacity: number | string; } diff --git a/src/containers/Cluster/ClusterOverview/utils.tsx b/src/containers/Cluster/ClusterOverview/utils.tsx new file mode 100644 index 000000000..ea77e02cd --- /dev/null +++ b/src/containers/Cluster/ClusterOverview/utils.tsx @@ -0,0 +1,62 @@ +import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types'; +import {formatPercent} from '../../../utils/dataFormatters/dataFormatters'; +import {calculateProgressStatus} from '../../../utils/progress'; + +import type {ClusterMetricsBaseProps, ClusterMetricsCommonProps} from './shared'; + +export function calculateBaseDiagramValues({ + colorizeProgress = true, + warningThreshold, + dangerThreshold, + inverseColorize = false, + fillWidth, +}: ClusterMetricsBaseProps & {fillWidth: number}) { + const normalizedFillWidth = fillWidth < 1 ? 0.5 : fillWidth; + const status = calculateProgressStatus({ + fillWidth, + warningThreshold, + dangerThreshold, + colorizeProgress, + inverseColorize, + }); + + const percents = formatPercent(fillWidth / 100); + + return {status, percents, fill: normalizedFillWidth}; +} + +export function getDiagramValues({ + value, + capacity, + legendFormatter, + ...rest +}: ClusterMetricsCommonProps & { + legendFormatter: (params: {value: number; capacity: number}) => string; +}) { + const parsedValue = parseFloat(String(value)); + const parsedCapacity = parseFloat(String(capacity)); + let fillWidth = (parsedValue / parsedCapacity) * 100 || 0; + fillWidth = fillWidth > 100 ? 100 : fillWidth; + + const legend = legendFormatter({ + value: parsedValue, + capacity: parsedCapacity, + }); + return { + ...calculateBaseDiagramValues({ + fillWidth, + ...rest, + }), + legend, + }; +} + +export const getTotalStorageGroupsUsed = (groupStats: ClusterGroupsStats) => { + return Object.values(groupStats).reduce((acc, data) => { + Object.values(data).forEach((erasureStats) => { + acc += erasureStats.createdGroups; + }); + + return acc; + }, 0); +}; diff --git a/src/containers/Cluster/i18n/en.json b/src/containers/Cluster/i18n/en.json index 3f318f12b..3d4f5b93c 100644 --- a/src/containers/Cluster/i18n/en.json +++ b/src/containers/Cluster/i18n/en.json @@ -6,8 +6,6 @@ "usage": "Usage", "label_nodes-state": "Nodes state", "label_dc": "Nodes data centers", - "storage-size": "Storage size", - "storage-groups": "Storage groups, {{diskType}}", "links": "Links", "link_cores": "Coredumps", "link_logging": "Logging", @@ -16,11 +14,21 @@ "title_cpu": "CPU", "title_storage": "Storage", "title_memory": "Memory", - "title_info": "Info", "title_links": "Links", - "label_nodes": "Nodes", - "label_hosts": "Hosts", - "label_storage-groups": "Storage groups", - "label_databases": "Databases", - "label_load": "Load" + "label_overview": "Overview", + "label_load": "Load", + "context_of": "of", + "context_cpu": "CPU load", + "context_memory": "Memory used", + "context_storage": [ + "Storage: {{count}} group", + "Storage: {{count}} groups", + "Storage: {{count}} groups", + "Storage: {{count}} groups" + ], + "context_network": "Network Evaluation", + "context_cpu-description": "CPU load is calculated as the cumulative usage across all actor system pools on all nodes in the cluster", + "context_memory-description": "Memory usage is the total memory consumed by all nodes in the cluster", + "context_storage-description": "Storage usage is a cumulative usage of raw disk space of all media types", + "context_network-description": "Network usage is the average outgoing bandwidth usage across all nodes in the cluster" } diff --git a/src/containers/Cluster/utils.tsx b/src/containers/Cluster/utils.tsx index 68358552e..4e22efc3c 100644 --- a/src/containers/Cluster/utils.tsx +++ b/src/containers/Cluster/utils.tsx @@ -2,7 +2,6 @@ import routes, {createHref} from '../../routes'; import type {ValueOf} from '../../types/common'; export const clusterTabsIds = { - overview: 'overview', tenants: 'tenants', nodes: 'nodes', storage: 'storage', @@ -12,11 +11,6 @@ export const clusterTabsIds = { export type ClusterTab = ValueOf; -const overview = { - id: clusterTabsIds.overview, - title: 'Overview', -}; - const tenants = { id: clusterTabsIds.tenants, title: 'Databases', @@ -38,7 +32,7 @@ const tablets = { title: 'Tablets', }; -export const clusterTabs = [overview, tenants, nodes, storage, tablets, versions]; +export const clusterTabs = [tenants, nodes, storage, tablets, versions]; export function isClusterTab(tab: any): tab is ClusterTab { return Object.values(clusterTabsIds).includes(tab); diff --git a/src/containers/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json index cfc868b31..638c0b82d 100644 --- a/src/containers/UserSettings/i18n/en.json +++ b/src/containers/UserSettings/i18n/en.json @@ -38,6 +38,8 @@ "settings.enableNetworkTable.title": "Enable network table", + "settings.showNetworkUtilization.title": "Show cluster network utilization", + "settings.useShowPlanToSvg.title": "Execution plan", "settings.useShowPlanToSvg.description": " Show \"Execution plan\" button in query result widow. Opens svg with execution plan in a new window.", diff --git a/src/containers/UserSettings/settings.tsx b/src/containers/UserSettings/settings.tsx index 5a8692afe..7392c99a4 100644 --- a/src/containers/UserSettings/settings.tsx +++ b/src/containers/UserSettings/settings.tsx @@ -12,6 +12,7 @@ import { INVERTED_DISKS_KEY, LANGUAGE_KEY, SHOW_DOMAIN_DATABASE_KEY, + SHOW_NETWORK_UTILIZATION, THEME_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, USE_SHOW_PLAN_SVG_KEY, @@ -134,6 +135,11 @@ export const enableQueryStreamingSetting: SettingProps = { description: i18n('settings.editor.queryStreaming.description'), }; +export const showNetworkUtilizationSetting: SettingProps = { + settingKey: SHOW_NETWORK_UTILIZATION, + title: i18n('settings.showNetworkUtilization.title'), +}; + export const autocompleteOnEnterSetting: SettingProps = { settingKey: AUTOCOMPLETE_ON_ENTER, title: i18n('settings.editor.autocomplete-on-enter.title'), @@ -160,7 +166,12 @@ export const appearanceSection: SettingsSection = { export const experimentsSection: SettingsSection = { id: 'experimentsSection', title: i18n('section.experiments'), - settings: [enableNetworkTable, useShowPlanToSvgTables, enableQueryStreamingSetting], + settings: [ + enableNetworkTable, + useShowPlanToSvgTables, + enableQueryStreamingSetting, + showNetworkUtilizationSetting, + ], }; export const devSettingsSection: SettingsSection = { diff --git a/src/services/settings.ts b/src/services/settings.ts index cca3cfb14..e4f70a9ff 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -9,6 +9,7 @@ import { ENABLE_CODE_ASSISTANT, ENABLE_NETWORK_TABLE_KEY, ENABLE_QUERY_STREAMING, + EXPAND_CLUSTER_DASHBOARD, INVERTED_DISKS_KEY, IS_HOTKEYS_HELP_HIDDEN_KEY, LANGUAGE_KEY, @@ -20,6 +21,7 @@ import { QUERY_STOPPED_BANNER_CLOSED_KEY, SAVED_QUERIES_KEY, SHOW_DOMAIN_DATABASE_KEY, + SHOW_NETWORK_UTILIZATION, TENANT_INITIAL_PAGE_KEY, THEME_KEY, USE_CLUSTER_BALANCER_AS_BACKEND_KEY, @@ -47,6 +49,8 @@ export const DEFAULT_USER_SETTINGS = { [ENABLE_AUTOCOMPLETE]: true, [ENABLE_CODE_ASSISTANT]: true, [ENABLE_QUERY_STREAMING]: true, + [SHOW_NETWORK_UTILIZATION]: false, + [EXPAND_CLUSTER_DASHBOARD]: true, [AUTOCOMPLETE_ON_ENTER]: true, [IS_HOTKEYS_HELP_HIDDEN_KEY]: false, [AUTO_REFRESH_INTERVAL]: 0, diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts index 753d5d7dc..7e9b9db79 100644 --- a/src/store/reducers/cluster/cluster.ts +++ b/src/store/reducers/cluster/cluster.ts @@ -29,7 +29,7 @@ let defaultClusterTab: ClusterTab; if (isClusterTab(defaultClusterTabLS)) { defaultClusterTab = defaultClusterTabLS; } else { - defaultClusterTab = clusterTabsIds.overview; + defaultClusterTab = clusterTabsIds.tenants; } const initialState: ClusterState = { diff --git a/src/types/api/cluster.ts b/src/types/api/cluster.ts index 21df2c143..ac44e8f78 100644 --- a/src/types/api/cluster.ts +++ b/src/types/api/cluster.ts @@ -74,10 +74,24 @@ export interface TClusterInfoV2 extends TClusterInfoV1 { CoresTotal?: number; } -export type TClusterInfo = TClusterInfoV1 | TClusterInfoV2; +export interface TClusterInfoV5 extends TClusterInfoV2 { + /** value is float */ + NetworkUtilization?: number; + /** value is uint64 */ + NetworkWriteThroughput?: string; +} + +export type TClusterInfo = TClusterInfoV1 | TClusterInfoV2 | TClusterInfoV5; export function isClusterInfoV2(info?: TClusterInfo): info is TClusterInfoV2 { - return info - ? 'Version' in info && typeof info.Version === 'number' && info.Version >= 2 - : false; + return isClusterParticularVersionOrHigher(info, 2); +} +export function isClusterInfoV5(info?: TClusterInfo): info is TClusterInfoV5 { + return isClusterParticularVersionOrHigher(info, 5); +} + +function isClusterParticularVersionOrHigher(info: TClusterInfo | undefined, version: number) { + return Boolean( + info && 'Version' in info && typeof info.Version === 'number' && info.Version >= version, + ); } diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 808f9aa5a..70d9ba821 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -130,3 +130,7 @@ export const AUTOCOMPLETE_ON_ENTER = 'autocompleteOnEnter'; export const IS_HOTKEYS_HELP_HIDDEN_KEY = 'isHotKeysHelpHidden'; export const DEV_ENABLE_TRACING_FOR_ALL_REQUESTS = 'enable_tracing_for_all_requests'; + +export const SHOW_NETWORK_UTILIZATION = 'enableNetworkUtilization'; + +export const EXPAND_CLUSTER_DASHBOARD = 'expandClusterDashboard';