diff --git a/src/components/EntityStatusNew/EntityStatus.scss b/src/components/EntityStatusNew/EntityStatus.scss
new file mode 100644
index 0000000000..64b2221c68
--- /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 0000000000..dd923cbc75
--- /dev/null
+++ b/src/components/EntityStatusNew/EntityStatus.tsx
@@ -0,0 +1,97 @@
+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'];
+ iconSize?: number;
+}
+
+function EntityStatusLabel({
+ children,
+ status,
+ withStatusName = true,
+ note,
+ size = 'm',
+ iconSize = 14,
+}: EntityStatusLabelProps) {
+ const theme = EFlagToLabelTheme[status];
+ return (
+
+ }
+ size={size}
+ className={b({orange: theme === 'orange'})}
+ >
+
+ {children}
+ {withStatusName ? EFlagToStatusName[status] : null}
+ {note && {note}}
+
+
+
+ );
+}
+
+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 0000000000..896df0ec34
--- /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 0000000000..176f01344d
--- /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/utils.ts b/src/components/EntityStatusNew/utils.ts
new file mode 100644
index 0000000000..16a896fdfd
--- /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 0000000000..7808c7edc1
--- /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 04d131492d..0000000000
--- 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 bd28065543..0000000000
--- 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 9790fcbf11..0000000000
--- 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 3030b45f37..7e02aca9dd 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 892593ebc4..2bfc51b492 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 60fe1bd6b1..0000000000
--- 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 7959395bdb..0000000000
--- 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 07cb6e5393..0000000000
--- 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 78de7fc0ac..0000000000
--- 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 3e0555f10c..fbd17cacad 100644
--- a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss
+++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss
@@ -1,19 +1,13 @@
@use '../../../styles/mixins';
.cluster-info {
- padding: 20px 0;
-
- @include mixins.body-2-typography();
+ --g-definition-list-item-gap: var(--g-spacing-3);
+ padding: var(--g-spacing-4) 0 var(--g-spacing-2);
&__skeleton {
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 13cff38e1a..982f91285a 100644
--- a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx
+++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx
@@ -1,24 +1,30 @@
-import {DefinitionList, Flex} from '@gravity-ui/uikit';
+import React from 'react';
+
+import {DefinitionList, Flex, Text} from '@gravity-ui/uikit';
import {ResponseError} from '../../../components/Errors/ResponseError';
import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton';
import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon';
+import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types';
import type {AdditionalClusterProps} from '../../../types/additionalProps';
import type {TClusterInfo} from '../../../types/api/cluster';
import type {IResponseError} from '../../../types/api/error';
+import {formatNumber} from '../../../utils/dataFormatters/dataFormatters';
import i18n from '../i18n';
+import {getTotalStorageGroupsUsed} from '../utils';
import {b} from './shared';
import {useClusterLinks} from './utils/useClusterLinks';
-import {getInfo} from './utils/utils';
+import {getInfo, getStorageGroupStats} from './utils/utils';
import './ClusterInfo.scss';
interface ClusterInfoProps {
cluster?: TClusterInfo;
loading?: boolean;
- error?: IResponseError;
+ error?: IResponseError | string;
additionalClusterProps?: AdditionalClusterProps;
+ groupStats?: ClusterGroupsStats;
}
export const ClusterInfo = ({
@@ -26,6 +32,7 @@ export const ClusterInfo = ({
loading,
error,
additionalClusterProps = {},
+ groupStats = {},
}: ClusterInfoProps) => {
const {info = [], links = []} = additionalClusterProps;
@@ -34,61 +41,89 @@ export const ClusterInfo = ({
const clusterInfo = getInfo(cluster ?? {}, info);
- const renderInfo = () => {
+ const renderDetails = () => {
if (error && !cluster) {
return null;
}
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 ;
- })}
-
-
- );
+ const renderDetailsContent = () => {
+ if (loading) {
+ return ;
}
- return null;
+ return renderDetails();
};
- const renderContent = () => {
+ const renderDetailSection = () => {
+ return (
+
+
+ {i18n('title_details')}
+
+ {renderDetailsContent()}
+
+ );
+ };
+
+ const total = getTotalStorageGroupsUsed(groupStats);
+
+ const renderGroupsInfoSection = () => {
+ const stats = getStorageGroupStats(groupStats);
if (loading) {
- return ;
+ return null;
}
-
return (
-
- {renderInfo()}
- {renderLinks()}
-
+
+
+ {i18n('title_storage-groups')}{' '}
+
+ {formatNumber(total)}
+
+
+ {stats}
+
);
};
return (
-
+
{error ? : null}
- {renderContent()}
-
+ {renderDetailSection()}
+ {renderGroupsInfoSection()}
+
);
};
+
+interface InfoSectionProps {
+ children: React.ReactNode;
+}
+
+function InfoSection({children}: InfoSectionProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStats.scss b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStats.scss
new file mode 100644
index 0000000000..988f279de8
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStats.scss
@@ -0,0 +1,14 @@
+.ydb-disk-groups-stats {
+ --g-definition-list-item-gap: var(--g-spacing-2);
+ width: 287px;
+ padding: var(--g-spacing-3) var(--g-spacing-4);
+
+ border-radius: var(--g-border-radius-s);
+ background-color: var(--g-color-base-generic-ultralight);
+
+ &__progress {
+ display: inline-block;
+
+ width: 60px;
+ }
+}
diff --git a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStats.tsx b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStats.tsx
new file mode 100644
index 0000000000..4941658bc8
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStats.tsx
@@ -0,0 +1,86 @@
+import type {ProgressTheme} from '@gravity-ui/uikit';
+import {DefinitionList, Flex, Progress, Text} from '@gravity-ui/uikit';
+
+import type {DiskErasureGroupsStats} from '../../../../../store/reducers/cluster/types';
+import {formatBytes, getBytesSizeUnit} from '../../../../../utils/bytesParsers';
+import {cn} from '../../../../../utils/cn';
+import {formatNumber} from '../../../../../utils/dataFormatters/dataFormatters';
+import type {ProgressStatus} from '../../../../../utils/progress';
+import {calculateProgressStatus} from '../../../../../utils/progress';
+import i18n from '../../../i18n';
+
+import './DiskGroupsStats.scss';
+
+const b = cn('ydb-disk-groups-stats');
+
+interface GroupsStatsPopupContentProps {
+ stats: DiskErasureGroupsStats;
+ storageType: string;
+}
+
+const calculatedStatusToProgressTheme: Record = {
+ good: 'success',
+ warning: 'warning',
+ danger: 'danger',
+};
+
+export function DiskGroupsStats({stats, storageType}: GroupsStatsPopupContentProps) {
+ const {erasure, allocatedSize, availableSize} = stats;
+
+ const sizeToConvert = getBytesSizeUnit(Math.max(allocatedSize, availableSize));
+
+ const convertedAllocatedSize = formatBytes({value: allocatedSize, size: sizeToConvert});
+ const convertedAvailableSize = formatBytes({value: availableSize, size: sizeToConvert});
+
+ const usage = Math.round((allocatedSize / (allocatedSize + availableSize)) * 100);
+
+ const info = [
+ {
+ name: i18n('erasure'),
+ content: erasure,
+ },
+ {
+ name: i18n('allocated'),
+ content: convertedAllocatedSize,
+ },
+ {
+ name: i18n('available'),
+ content: convertedAvailableSize,
+ },
+ {
+ name: i18n('usage'),
+ content: (
+
+
+ {usage}%
+
+ ),
+ },
+ ];
+ return (
+
+
+ {storageType}{' '}
+
+ {`${formatNumber(stats.createdGroups)} ${i18n('context_of')} ${formatNumber(stats.totalGroups)}`}
+
+
+
+ {info.map(({name, content}) => (
+
+ {content}
+
+ ))}
+
+
+ );
+}
diff --git a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss
deleted file mode 100644
index 92647ff851..0000000000
--- a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss
+++ /dev/null
@@ -1,6 +0,0 @@
-.ydb-disk-groups-stats {
- cursor: pointer;
- &__popup-content {
- padding: var(--g-spacing-3);
- }
-}
diff --git a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx
deleted file mode 100644
index f93790aaa3..0000000000
--- a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx
+++ /dev/null
@@ -1,70 +0,0 @@
-import {DefinitionList} from '@gravity-ui/components';
-
-import {ContentWithPopup} from '../../../../../components/ContentWithPopup/ContentWithPopup';
-import type {DiskErasureGroupsStats} from '../../../../../store/reducers/cluster/types';
-import {formatBytes, getBytesSizeUnit} from '../../../../../utils/bytesParsers';
-import {cn} from '../../../../../utils/cn';
-import i18n from '../../../i18n';
-
-import './DiskGroupsStatsBars.scss';
-
-const b = cn('ydb-disk-groups-stats');
-
-interface DiskGroupsStatsProps {
- stats: DiskErasureGroupsStats;
- children: React.ReactNode;
-}
-
-export const DiskGroupsErasureStats = ({stats, children}: DiskGroupsStatsProps) => {
- return (
-
- }
- >
- {children}
-
-
- );
-};
-
-interface GroupsStatsPopupContentProps {
- stats: DiskErasureGroupsStats;
-}
-
-function GroupsStatsPopupContent({stats}: GroupsStatsPopupContentProps) {
- const {diskType, erasure, allocatedSize, availableSize} = stats;
-
- const sizeToConvert = getBytesSizeUnit(Math.max(allocatedSize, availableSize));
-
- const convertedAllocatedSize = formatBytes({value: allocatedSize, size: sizeToConvert});
- const convertedAvailableSize = formatBytes({value: availableSize, size: sizeToConvert});
-
- const usage = Math.round((allocatedSize / (allocatedSize + availableSize)) * 100);
-
- const info = [
- {
- name: i18n('disk-type'),
- content: diskType,
- },
- {
- name: i18n('erasure'),
- content: erasure,
- },
- {
- name: i18n('allocated'),
- content: convertedAllocatedSize,
- },
- {
- name: i18n('available'),
- content: convertedAvailableSize,
- },
- {
- name: i18n('usage'),
- content: usage + '%',
- },
- ];
-
- 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 7db8895502..0000000000
--- 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 bd1557e0ca..0000000000
--- 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 e51934a51d..fc9146834c 100644
--- a/src/containers/Cluster/ClusterInfo/utils/utils.tsx
+++ b/src/containers/Cluster/ClusterInfo/utils/utils.tsx
@@ -2,15 +2,17 @@ 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 type {ClusterGroupsStats} from '../../../../store/reducers/cluster/types';
import {isClusterInfoV2} from '../../../../types/api/cluster';
import type {TClusterInfo} from '../../../../types/api/cluster';
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 {DiskGroupsStats} from '../components/DiskGroupsStatsBars/DiskGroupsStats';
import {b} from '../shared';
const COLORS_PRIORITY: Record = {
@@ -26,7 +28,7 @@ const getDCInfo = (cluster: TClusterInfo) => {
if (isClusterInfoV2(cluster) && cluster.MapDataCenters) {
return Object.entries(cluster.MapDataCenters).map(([dc, count]) => (
- {dc}: {formatNumber(count)}
+ {dc} : {formatNumber(count)}
));
}
@@ -36,28 +38,34 @@ const getDCInfo = (cluster: TClusterInfo) => {
export const getInfo = (cluster: TClusterInfo, additionalInfo: InfoItem[]) => {
const info: InfoItem[] = [];
+ const dataCenters = getDCInfo(cluster);
+ if (dataCenters?.length) {
+ info.push({
+ label: i18n('label_dc'),
+ value: ,
+ });
+ }
+
if (isClusterInfoV2(cluster) && cluster.MapNodeStates) {
const arrayNodesStates = Object.entries(cluster.MapNodeStates) as [EFlag, number][];
// sort stack to achieve order "green, orange, yellow, red, blue, grey"
arrayNodesStates.sort((a, b) => COLORS_PRIORITY[b[0]] - COLORS_PRIORITY[a[0]]);
const nodesStates = arrayNodesStates.map(([state, count]) => {
return (
-
+
{formatNumber(count)}
-
+
);
});
info.push({
label: i18n('label_nodes-state'),
- value: {nodesStates},
- });
- }
-
- const dataCenters = getDCInfo(cluster);
- if (dataCenters?.length) {
- info.push({
- label: i18n('label_dc'),
- value: ,
+ value: {nodesStates},
});
}
@@ -75,3 +83,20 @@ export const getInfo = (cluster: TClusterInfo, additionalInfo: InfoItem[]) => {
return info;
};
+
+export function getStorageGroupStats(groupStats: ClusterGroupsStats) {
+ const result: React.ReactNode[] = [];
+
+ Object.entries(groupStats).forEach(([storageType, stats]) => {
+ Object.values(stats).forEach((erasureStats) => {
+ result.push(
+ ,
+ );
+ });
+ });
+ return result;
+}
diff --git a/src/containers/Cluster/ClusterOverview/ClusterOverview.scss b/src/containers/Cluster/ClusterOverview/ClusterOverview.scss
new file mode 100644
index 0000000000..b8f160e77f
--- /dev/null
+++ b/src/containers/Cluster/ClusterOverview/ClusterOverview.scss
@@ -0,0 +1,70 @@
+@use '../../../styles/mixins.scss';
+
+.ydb-cluster-dashboard {
+ &__dashboard-wrapper {
+ gap: var(--g-spacing-6);
+
+ padding-top: var(--g-spacing-4);
+ }
+
+ &__dashboard-wrapper_collapsed {
+ gap: var(--g-spacing-1);
+
+ 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%;
+ }
+ }
+
+ &__card {
+ min-width: 280px;
+ height: 132px;
+ }
+}
diff --git a/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx b/src/containers/Cluster/ClusterOverview/ClusterOverview.tsx
new file mode 100644
index 0000000000..c4c1bb87e8
--- /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 {getTotalStorageGroupsUsed} from '../utils';
+
+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 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 0000000000..430265d9b2
--- /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 fc87918eab..571c1a1dce 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 0000000000..12eb109754
--- /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 0000000000..ab61aab06d
--- /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 0000000000..e5d8c9011c
--- /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 23591c8fdc..ae3ef09e03 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 0000000000..e66b5a52c8
--- /dev/null
+++ b/src/containers/Cluster/ClusterOverview/utils.tsx
@@ -0,0 +1,51 @@
+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 = Math.max(fillWidth, 0.5);
+ 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,
+ };
+}
diff --git a/src/containers/Cluster/i18n/en.json b/src/containers/Cluster/i18n/en.json
index 3f318f12b5..18e8a46119 100644
--- a/src/containers/Cluster/i18n/en.json
+++ b/src/containers/Cluster/i18n/en.json
@@ -1,13 +1,10 @@
{
- "disk-type": "Disk Type",
"erasure": "Erasure",
"allocated": "Allocated",
"available": "Available",
"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",
@@ -15,12 +12,25 @@
"context_cores": "cores",
"title_cpu": "CPU",
"title_storage": "Storage",
+ "title_storage-groups": "Storage Groups",
"title_memory": "Memory",
- "title_info": "Info",
+ "title_network": "Network",
"title_links": "Links",
- "label_nodes": "Nodes",
- "label_hosts": "Hosts",
- "label_storage-groups": "Storage groups",
- "label_databases": "Databases",
- "label_load": "Load"
+ "title_details": "Details",
+ "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 bdb01840dc..785ebfda94 100644
--- a/src/containers/Cluster/utils.tsx
+++ b/src/containers/Cluster/utils.tsx
@@ -1,9 +1,9 @@
import type {CreateHrefOptions} from '../../routes';
import routes, {createHref} from '../../routes';
+import type {ClusterGroupsStats} from '../../store/reducers/cluster/types';
import type {ValueOf} from '../../types/common';
export const clusterTabsIds = {
- overview: 'overview',
tenants: 'tenants',
nodes: 'nodes',
storage: 'storage',
@@ -13,11 +13,6 @@ export const clusterTabsIds = {
export type ClusterTab = ValueOf;
-const overview = {
- id: clusterTabsIds.overview,
- title: 'Overview',
-};
-
const tenants = {
id: clusterTabsIds.tenants,
title: 'Databases',
@@ -39,7 +34,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);
@@ -48,3 +43,13 @@ export function isClusterTab(tab: any): tab is ClusterTab {
export const getClusterPath = (activeTab?: ClusterTab, query = {}, options?: CreateHrefOptions) => {
return createHref(routes.cluster, activeTab ? {activeTab} : undefined, query, options);
};
+
+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/UserSettings/i18n/en.json b/src/containers/UserSettings/i18n/en.json
index cfc868b31a..638c0b82db 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 5a8692afe5..7392c99a40 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 cca3cfb144..e4f70a9ff0 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 cdd53c70e7..a8a378ed7f 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 21df2c1435..ac44e8f78e 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 808f9aa5a6..70d9ba8212 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';