diff --git a/package-lock.json b/package-lock.json
index bc8aac7354..0cbe30f340 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8106,9 +8106,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001582",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001582.tgz",
- "integrity": "sha512-vsJG3V5vgfduaQGVxL53uSX/HUzxyr2eA8xCo36OLal7sRcSZbibJtLeh0qja4sFOr/QQGt4opB4tOy+eOgAxg==",
+ "version": "1.0.30001660",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz",
+ "integrity": "sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==",
"dev": true,
"funding": [
{
diff --git a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss
index ae50905501..cdb2a2074e 100644
--- a/src/components/DiskStateProgressBar/DiskStateProgressBar.scss
+++ b/src/components/DiskStateProgressBar/DiskStateProgressBar.scss
@@ -1,3 +1,5 @@
+@import '../../styles/mixins';
+
.storage-disk-progress-bar {
$block: &;
@@ -5,9 +7,6 @@
$outer-border-radius: 4px;
$inner-border-radius: $outer-border-radius - $border-width;
- --progress-bar-border-color: var(--g-color-base-misc-heavy);
- --progress-bar-background-color: var(--g-color-base-misc-light);
- --progress-bar-fill-color: var(--g-color-base-misc-medium);
--progress-bar-full-height: var(--g-text-body-3-line-height);
--progress-bar-compact-height: 12px;
@@ -20,9 +19,10 @@
text-align: center;
color: var(--g-color-text-primary);
- border: $border-width solid var(--progress-bar-border-color);
+ border: $border-width solid var(--entity-state-border-color);
border-radius: $outer-border-radius;
- background-color: var(--progress-bar-background-color);
+ background-color: var(--entity-state-background-color);
+ @include entity-state-colors();
&_compact {
min-width: 0;
@@ -31,35 +31,6 @@
border-radius: 2px;
}
- &_green {
- --progress-bar-border-color: var(--g-color-base-positive-heavy);
- --progress-bar-background-color: var(--g-color-base-positive-light);
- --progress-bar-fill-color: var(--g-color-base-positive-medium);
- }
-
- &_blue {
- --progress-bar-border-color: var(--g-color-base-info-heavy);
- --progress-bar-background-color: var(--g-color-base-info-light);
- --progress-bar-fill-color: var(--g-color-base-info-medium);
- }
-
- &_yellow {
- --progress-bar-border-color: var(--g-color-base-warning-heavy);
- --progress-bar-background-color: var(--g-color-base-yellow-light);
- --progress-bar-fill-color: var(--g-color-base-yellow-medium);
- }
-
- &_orange {
- --progress-bar-border-color: var(--ydb-color-status-orange);
- --progress-bar-background-color: var(--g-color-base-warning-light);
- --progress-bar-fill-color: var(--g-color-base-warning-medium);
- }
- &_red {
- --progress-bar-border-color: var(--g-color-base-danger-heavy);
- --progress-bar-background-color: var(--g-color-base-danger-light);
- --progress-bar-fill-color: var(--g-color-base-danger-medium);
- }
-
&_faded {
background-color: unset;
}
@@ -78,11 +49,11 @@
height: 100%;
border-radius: $inner-border-radius 0 0 $inner-border-radius;
- background-color: var(--progress-bar-fill-color);
+ background-color: var(--entity-state-fill-color);
&_faded {
// Bg color is light variant, use it to make bar less bright
- background-color: var(--progress-bar-background-color);
+ background-color: var(--entity-state-background-color);
}
&_compact {
diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx
index 241e91d1b1..bd28065543 100644
--- a/src/components/Tag/Tag.tsx
+++ b/src/components/Tag/Tag.tsx
@@ -1,3 +1,5 @@
+import React from 'react';
+
import {cn} from '../../utils/cn';
import './Tag.scss';
@@ -7,7 +9,7 @@ const b = cn('tag');
export type TagType = 'blue';
interface TagProps {
- text: string;
+ text: React.ReactNode;
type?: TagType;
}
diff --git a/src/components/Tags/Tags.tsx b/src/components/Tags/Tags.tsx
index ee0054dfbc..9a0c61d2b3 100644
--- a/src/components/Tags/Tags.tsx
+++ b/src/components/Tags/Tags.tsx
@@ -1,3 +1,5 @@
+import React from 'react';
+
import {cn} from '../../utils/cn';
import type {TagType} from '../Tag';
import {Tag} from '../Tag';
@@ -7,7 +9,7 @@ import './Tags.scss';
const b = cn('tags');
interface TagsProps {
- tags: string[];
+ tags: React.ReactNode[];
tagsType?: TagType;
className?: string;
}
diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss
index b4ebe35a26..e8e3d29449 100644
--- a/src/containers/Cluster/ClusterInfo/ClusterInfo.scss
+++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.scss
@@ -47,24 +47,17 @@
gap: var(--g-spacing-2);
}
- &__storage-groups-stats {
- display: flex;
- flex-direction: column;
- gap: 11px;
- }
-
- &__groups-stats-bar {
- cursor: pointer;
- }
-
- &__groups-stats-popup-content {
- padding: 12px;
- }
-
&__clipboard-button {
display: flex;
align-items: center;
margin-left: 5px;
}
+ &__dc-count {
+ text-transform: lowercase;
+ }
+ &__nodes-states {
+ display: flex;
+ gap: var(--g-spacing-half);
+ }
}
diff --git a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx
index 857e386628..c777365e12 100644
--- a/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx
+++ b/src/containers/Cluster/ClusterInfo/ClusterInfo.tsx
@@ -1,198 +1,20 @@
-import React from 'react';
-
-import {ContentWithPopup} from '../../../components/ContentWithPopup/ContentWithPopup';
import {ResponseError} from '../../../components/Errors/ResponseError';
-import type {InfoViewerItem} from '../../../components/InfoViewer/InfoViewer';
import {InfoViewer} from '../../../components/InfoViewer/InfoViewer';
import {InfoViewerSkeleton} from '../../../components/InfoViewerSkeleton/InfoViewerSkeleton';
-import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon';
-import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer';
-import {Tablet} from '../../../components/Tablet';
-import {Tags} from '../../../components/Tags';
import {backend, customBackend} from '../../../store';
-import type {
- ClusterGroupsStats,
- DiskErasureGroupsStats,
- DiskGroupsStats,
-} from '../../../store/reducers/cluster/types';
-import {nodesApi} from '../../../store/reducers/nodes/nodes';
-import type {AdditionalClusterProps, ClusterLink} from '../../../types/additionalProps';
+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 type {VersionToColorMap, VersionValue} from '../../../types/versions';
-import {formatBytes, getSizeWithSignificantDigits} from '../../../utils/bytesParsers';
-import {cn} from '../../../utils/cn';
+import type {VersionToColorMap} from '../../../types/versions';
import {DEVELOPER_UI_TITLE} from '../../../utils/constants';
-import {formatStorageValues} from '../../../utils/dataFormatters/dataFormatters';
import {useTypedSelector} from '../../../utils/hooks';
-import {parseNodeGroupsToVersionsValues, parseNodesToVersionsValues} from '../../../utils/versions';
-import {VersionsBar} from '../VersionsBar/VersionsBar';
-import i18n from '../i18n';
-import {compareTablets} from './utils';
+import {b} from './shared';
+import {getInfo, useGetVersionValues} from './utils';
import './ClusterInfo.scss';
-const b = cn('cluster-info');
-
-interface GroupsStatsPopupContentProps {
- stats: DiskErasureGroupsStats;
-}
-
-const GroupsStatsPopupContent = ({stats}: GroupsStatsPopupContentProps) => {
- const {diskType, erasure, allocatedSize, availableSize} = stats;
-
- const sizeToConvert = getSizeWithSignificantDigits(Math.max(allocatedSize, availableSize), 2);
-
- const convertedAllocatedSize = formatBytes({value: allocatedSize, size: sizeToConvert});
- const convertedAvailableSize = formatBytes({value: availableSize, size: sizeToConvert});
-
- const usage = Math.round((allocatedSize / (allocatedSize + availableSize)) * 100);
-
- const info = [
- {
- label: i18n('disk-type'),
- value: diskType,
- },
- {
- label: i18n('erasure'),
- value: erasure,
- },
- {
- label: i18n('allocated'),
- value: convertedAllocatedSize,
- },
- {
- label: i18n('available'),
- value: convertedAvailableSize,
- },
- {
- label: i18n('usage'),
- value: usage + '%',
- },
- ];
-
- return (
-
- );
-};
-
-interface DiskGroupsStatsProps {
- stats: DiskGroupsStats;
-}
-
-const DiskGroupsStatsBars = ({stats}: DiskGroupsStatsProps) => {
- return (
-
- {Object.values(stats).map((erasureStats) => (
-
}
- >
-
-
- ))}
-
- );
-};
-
-const getGroupsStatsFields = (groupsStats: ClusterGroupsStats) => {
- return Object.keys(groupsStats).map((diskType) => {
- return {
- label: i18n('storage-groups', {diskType}),
- value: ,
- };
- });
-};
-
-const getInfo = (
- cluster: TClusterInfo,
- versionsValues: VersionValue[],
- groupsStats: ClusterGroupsStats,
- additionalInfo: InfoViewerItem[],
- links: ClusterLink[],
-) => {
- const info: InfoViewerItem[] = [];
-
- if (cluster.DataCenters) {
- info.push({
- label: i18n('dc'),
- value: ,
- });
- }
-
- if (cluster.SystemTablets) {
- const tablets = cluster.SystemTablets.slice(0).sort(compareTablets);
- info.push({
- label: i18n('tablets'),
- value: (
-
- {tablets.map((tablet, tabletIndex) => (
-
- ))}
-
- ),
- });
- }
-
- if (cluster.Tenants) {
- info.push({
- label: i18n('databases'),
- value: cluster.Tenants,
- });
- }
-
- info.push(
- {
- label: i18n('nodes'),
- value: ,
- },
- {
- label: i18n('load'),
- value: ,
- },
- {
- label: i18n('storage-size'),
- value: (
-
- ),
- },
- );
-
- if (Object.keys(groupsStats).length) {
- info.push(...getGroupsStatsFields(groupsStats));
- }
-
- info.push(
- ...additionalInfo,
- {
- label: i18n('links'),
- value: (
-
- {links.map(({title, url}) => (
-
- ))}
-
- ),
- },
- {
- label: i18n('versions'),
- value: ,
- },
- );
-
- return info;
-};
-
interface ClusterInfoProps {
cluster?: TClusterInfo;
versionToColor?: VersionToColorMap;
@@ -212,20 +34,7 @@ export const ClusterInfo = ({
}: ClusterInfoProps) => {
const singleClusterMode = useTypedSelector((state) => state.singleClusterMode);
- const {currentData} = nodesApi.useGetNodesQuery({
- tablets: false,
- group: 'Version',
- });
-
- const versionsValues = React.useMemo(() => {
- if (!currentData) {
- return [];
- }
- if (Array.isArray(currentData.NodeGroups)) {
- return parseNodeGroupsToVersionsValues(currentData.NodeGroups, versionToColor);
- }
- return parseNodesToVersionsValues(currentData.Nodes, versionToColor);
- }, [currentData, versionToColor]);
+ const versionsValues = useGetVersionValues(cluster, versionToColor);
let internalLink = backend + '/internal';
diff --git a/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss
new file mode 100644
index 0000000000..1e7378a206
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.scss
@@ -0,0 +1,11 @@
+.ydb-disk-groups-stats {
+ display: flex;
+ flex-direction: column;
+ gap: var(--g-spacing-3);
+ &__bar {
+ 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
new file mode 100644
index 0000000000..d9013dcede
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/components/DiskGroupsStatsBars/DiskGroupsStatsBars.tsx
@@ -0,0 +1,78 @@
+import {ContentWithPopup} from '../../../../../components/ContentWithPopup/ContentWithPopup';
+import {InfoViewer} from '../../../../../components/InfoViewer';
+import {ProgressViewer} from '../../../../../components/ProgressViewer/ProgressViewer';
+import type {
+ DiskErasureGroupsStats,
+ DiskGroupsStats,
+} from '../../../../../store/reducers/cluster/types';
+import {formatBytes, getSizeWithSignificantDigits} 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: DiskGroupsStats;
+}
+
+export const DiskGroupsStatsBars = ({stats}: DiskGroupsStatsProps) => {
+ return (
+
+ {Object.values(stats).map((erasureStats) => (
+
}
+ >
+
+
+ ))}
+
+ );
+};
+
+interface GroupsStatsPopupContentProps {
+ stats: DiskErasureGroupsStats;
+}
+
+function GroupsStatsPopupContent({stats}: GroupsStatsPopupContentProps) {
+ const {diskType, erasure, allocatedSize, availableSize} = stats;
+
+ const sizeToConvert = getSizeWithSignificantDigits(Math.max(allocatedSize, availableSize), 2);
+
+ const convertedAllocatedSize = formatBytes({value: allocatedSize, size: sizeToConvert});
+ const convertedAvailableSize = formatBytes({value: availableSize, size: sizeToConvert});
+
+ const usage = Math.round((allocatedSize / (allocatedSize + availableSize)) * 100);
+
+ const info = [
+ {
+ label: i18n('disk-type'),
+ value: diskType,
+ },
+ {
+ label: i18n('erasure'),
+ value: erasure,
+ },
+ {
+ label: i18n('allocated'),
+ value: convertedAllocatedSize,
+ },
+ {
+ label: i18n('available'),
+ value: convertedAvailableSize,
+ },
+ {
+ label: i18n('usage'),
+ value: usage + '%',
+ },
+ ];
+
+ return ;
+}
diff --git a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss
new file mode 100644
index 0000000000..e32f77726b
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.scss
@@ -0,0 +1,16 @@
+@import '../../../../../styles/mixins.scss';
+
+.ydb-nodes-state {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ min-width: 30px;
+ padding: 0 var(--g-spacing-1);
+
+ color: var(--entity-state-font-color);
+ border: 1px solid var(--entity-state-border-color);
+ border-radius: var(--g-spacing-1);
+ background-color: var(--entity-state-background-color);
+ @include entity-state-colors();
+}
diff --git a/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.tsx b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.tsx
new file mode 100644
index 0000000000..bd1557e0ca
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/components/NodesState/NodesState.tsx
@@ -0,0 +1,15 @@
+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/shared.ts b/src/containers/Cluster/ClusterInfo/shared.ts
new file mode 100644
index 0000000000..9deca58052
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/shared.ts
@@ -0,0 +1,3 @@
+import {cn} from '../../../utils/cn';
+
+export const b = cn('cluster-info');
diff --git a/src/containers/Cluster/ClusterInfo/utils.ts b/src/containers/Cluster/ClusterInfo/utils.ts
deleted file mode 100644
index 7a34e9f871..0000000000
--- a/src/containers/Cluster/ClusterInfo/utils.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import type {TTabletStateInfo} from '../../../types/api/tablet';
-import {EType} from '../../../types/api/tablet';
-
-export const compareTablets = (tablet1: TTabletStateInfo, tablet2: TTabletStateInfo) => {
- if (tablet1.Type === EType.TxAllocator) {
- return 1;
- }
-
- if (tablet2.Type === EType.TxAllocator) {
- return -1;
- }
-
- return 0;
-};
diff --git a/src/containers/Cluster/ClusterInfo/utils.tsx b/src/containers/Cluster/ClusterInfo/utils.tsx
new file mode 100644
index 0000000000..4a7b4b49d2
--- /dev/null
+++ b/src/containers/Cluster/ClusterInfo/utils.tsx
@@ -0,0 +1,237 @@
+import React from 'react';
+
+import {skipToken} from '@reduxjs/toolkit/query';
+
+import type {InfoViewerItem} from '../../../components/InfoViewer/InfoViewer';
+import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon';
+import {ProgressViewer} from '../../../components/ProgressViewer/ProgressViewer';
+import {Tablet} from '../../../components/Tablet';
+import {Tags} from '../../../components/Tags';
+import type {ClusterGroupsStats} from '../../../store/reducers/cluster/types';
+import {nodesApi} from '../../../store/reducers/nodes/nodes';
+import type {ClusterLink} from '../../../types/additionalProps';
+import {isClusterInfoV2} from '../../../types/api/cluster';
+import type {TClusterInfo} from '../../../types/api/cluster';
+import type {EFlag} from '../../../types/api/enums';
+import type {TTabletStateInfo} from '../../../types/api/tablet';
+import {EType} from '../../../types/api/tablet';
+import type {VersionToColorMap, VersionValue} from '../../../types/versions';
+import {formatStorageValues} from '../../../utils/dataFormatters/dataFormatters';
+import {parseNodeGroupsToVersionsValues, parseNodesToVersionsValues} from '../../../utils/versions';
+import {VersionsBar} from '../VersionsBar/VersionsBar';
+import i18n from '../i18n';
+
+import {DiskGroupsStatsBars} from './components/DiskGroupsStatsBars/DiskGroupsStatsBars';
+import {NodesState} from './components/NodesState/NodesState';
+import {b} from './shared';
+
+const COLORS_PRIORITY: Record = {
+ Green: 5,
+ Blue: 4,
+ Yellow: 3,
+ Orange: 2,
+ Red: 1,
+ Grey: 0,
+};
+
+export const compareTablets = (tablet1: TTabletStateInfo, tablet2: TTabletStateInfo) => {
+ if (tablet1.Type === EType.TxAllocator) {
+ return 1;
+ }
+
+ if (tablet2.Type === EType.TxAllocator) {
+ return -1;
+ }
+
+ return 0;
+};
+
+const getGroupsStatsFields = (groupsStats: ClusterGroupsStats) => {
+ return Object.keys(groupsStats).map((diskType) => {
+ return {
+ label: i18n('storage-groups', {diskType}),
+ value: ,
+ };
+ });
+};
+
+const getDCInfo = (cluster: TClusterInfo) => {
+ if (isClusterInfoV2(cluster) && cluster.MapDataCenters) {
+ return Object.entries(cluster.MapDataCenters).map(([dc, count]) => (
+
+ {dc}: {i18n('quantity', {count})}
+
+ ));
+ }
+ return cluster.DataCenters?.filter(Boolean);
+};
+
+const getStorageStats = (cluster: TClusterInfo) => {
+ if (isClusterInfoV2(cluster) && cluster.MapDataCenters) {
+ const {MapStorageTotal, MapStorageUsed} = cluster;
+ const storageTypesSet = new Set(
+ Object.keys(MapStorageTotal ?? []).concat(Object.keys(MapStorageUsed ?? [])),
+ );
+ if (storageTypesSet.size > 0) {
+ return Array.from(storageTypesSet).reduce(
+ (acc, storageType) => {
+ acc[storageType] = {
+ used: MapStorageUsed?.[storageType],
+ total: MapStorageTotal?.[storageType],
+ };
+ return acc;
+ },
+ {} as Record,
+ );
+ }
+ }
+ return {_default: {used: cluster?.StorageUsed, total: cluster?.StorageTotal}};
+};
+
+export const getInfo = (
+ cluster: TClusterInfo,
+ versionsValues: VersionValue[],
+ groupsStats: ClusterGroupsStats,
+ additionalInfo: InfoViewerItem[],
+ links: ClusterLink[],
+) => {
+ const info: InfoViewerItem[] = [];
+
+ const dataCenters = getDCInfo(cluster);
+
+ if (dataCenters?.length) {
+ info.push({
+ label: i18n('dc'),
+ value: ,
+ });
+ }
+
+ if (cluster.SystemTablets) {
+ const tablets = cluster.SystemTablets.slice(0).sort(compareTablets);
+ info.push({
+ label: i18n('tablets'),
+ value: (
+
+ {tablets.map((tablet, tabletIndex) => (
+
+ ))}
+
+ ),
+ });
+ }
+
+ if (cluster.Tenants) {
+ info.push({
+ label: i18n('databases'),
+ value: cluster.Tenants,
+ });
+ }
+
+ info.push({
+ label: i18n('nodes'),
+ 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 (
+
+ {count}
+
+ );
+ });
+ info.push({
+ label: i18n('nodes-state'),
+ value: {nodesStates}
,
+ });
+ }
+
+ info.push({
+ label: i18n('load'),
+ value: ,
+ });
+
+ const storageStats = getStorageStats(cluster);
+
+ Object.entries(storageStats).forEach(([type, stats]) => {
+ let label = i18n('storage-size');
+ if (type !== '_default') {
+ label += `, ${type}`;
+ }
+ info.push({
+ label: label,
+ value: (
+
+ ),
+ });
+ });
+
+ if (Object.keys(groupsStats).length) {
+ info.push(...getGroupsStatsFields(groupsStats));
+ }
+
+ info.push(
+ ...additionalInfo,
+ {
+ label: i18n('links'),
+ value: (
+
+ {links.map(({title, url}) => (
+
+ ))}
+
+ ),
+ },
+ {
+ label: i18n('versions'),
+ value: (
+ el.title !== 'unknown')}
+ />
+ ),
+ },
+ );
+
+ return info;
+};
+
+export const useGetVersionValues = (cluster?: TClusterInfo, versionToColor?: VersionToColorMap) => {
+ const {currentData} = nodesApi.useGetNodesQuery(
+ isClusterInfoV2(cluster)
+ ? skipToken
+ : {
+ tablets: false,
+ group: 'Version',
+ },
+ );
+
+ const versionsValues = React.useMemo(() => {
+ if (isClusterInfoV2(cluster) && cluster.MapVersions) {
+ const groups = Object.entries(cluster.MapVersions).map(([version, count]) => ({
+ name: version,
+ count,
+ }));
+ return parseNodeGroupsToVersionsValues(groups, versionToColor, cluster.NodesTotal);
+ }
+ if (!currentData) {
+ return [];
+ }
+ if (Array.isArray(currentData.NodeGroups)) {
+ return parseNodeGroupsToVersionsValues(
+ currentData.NodeGroups,
+ versionToColor,
+ cluster?.NodesTotal,
+ );
+ }
+ return parseNodesToVersionsValues(currentData.Nodes, versionToColor);
+ }, [currentData, versionToColor, cluster]);
+
+ return versionsValues;
+};
diff --git a/src/containers/Cluster/i18n/en.json b/src/containers/Cluster/i18n/en.json
index 6ec02cd409..cf0eba22d4 100644
--- a/src/containers/Cluster/i18n/en.json
+++ b/src/containers/Cluster/i18n/en.json
@@ -8,9 +8,15 @@
"tablets": "Tablets",
"databases": "Databases",
"nodes": "Nodes",
+ "nodes-state": "Nodes state",
"load": "Load",
"storage-size": "Storage size",
"storage-groups": "Storage groups, {{diskType}}",
"links": "Links",
- "versions": "Versions"
+ "versions": "Versions",
+ "quantity": {
+ "one": "{{count}} node",
+ "other": "{{count}} nodes",
+ "zero": "no nodes"
+ }
}
diff --git a/src/containers/Cluster/i18n/index.ts b/src/containers/Cluster/i18n/index.ts
index 758b317b3e..483b79634f 100644
--- a/src/containers/Cluster/i18n/index.ts
+++ b/src/containers/Cluster/i18n/index.ts
@@ -1,8 +1,7 @@
import {registerKeysets} from '../../../utils/i18n';
import en from './en.json';
-import ru from './ru.json';
const COMPONENT = 'ydb-cluster';
-export default registerKeysets(COMPONENT, {ru, en});
+export default registerKeysets(COMPONENT, {en});
diff --git a/src/containers/Cluster/i18n/ru.json b/src/containers/Cluster/i18n/ru.json
deleted file mode 100644
index 8e75e63add..0000000000
--- a/src/containers/Cluster/i18n/ru.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "disk-type": "Тип диска",
- "erasure": "Режим",
- "allocated": "Использовано",
- "available": "Доступно",
- "usage": "Потребление",
- "dc": "ДЦ",
- "tablets": "Таблетки",
- "databases": "Базы данных",
- "nodes": "Узлы",
- "load": "Нагрузка",
- "storage-size": "Размер хранилища",
- "storage-groups": "Группы хранения, {{diskType}}",
- "links": "Ссылки",
- "versions": "Версии"
-}
diff --git a/src/store/reducers/cluster/cluster.ts b/src/store/reducers/cluster/cluster.ts
index c10f5b07a8..90b70d8508 100644
--- a/src/store/reducers/cluster/cluster.ts
+++ b/src/store/reducers/cluster/cluster.ts
@@ -6,6 +6,7 @@ import {StringParam, useQueryParam} from 'use-query-params';
import type {ClusterTab} from '../../../containers/Cluster/utils';
import {clusterTabsIds, isClusterTab} from '../../../containers/Cluster/utils';
import {parseTraceFields} from '../../../services/parsers/parseMetaCluster';
+import {isClusterInfoV2} from '../../../types/api/cluster';
import type {TClusterInfo} from '../../../types/api/cluster';
import {CLUSTER_DEFAULT_TITLE, DEFAULT_CLUSTER_TAB_KEY} from '../../../utils/constants';
import {isQueryErrorResponse} from '../../../utils/query';
@@ -15,6 +16,7 @@ import {api} from '../api';
import type {ClusterGroupsStats, ClusterState} from './types';
import {
createSelectClusterGroupsQuery,
+ getGroupStatsFromClusterInfo,
normalizeDomain,
parseGroupsStatsQueryResponse,
} from './utils';
@@ -71,6 +73,15 @@ export const clusterApi = api.injectEndpoints({
return {data: {clusterData}};
}
+ if (isClusterInfoV2(clusterData)) {
+ return {
+ data: {
+ clusterData,
+ groupsStats: getGroupStatsFromClusterInfo(clusterData),
+ },
+ };
+ }
+
try {
const query = createSelectClusterGroupsQuery(clusterRoot);
diff --git a/src/store/reducers/cluster/utils.ts b/src/store/reducers/cluster/utils.ts
index 2dc9c9d767..d2fd4cd9e5 100644
--- a/src/store/reducers/cluster/utils.ts
+++ b/src/store/reducers/cluster/utils.ts
@@ -1,4 +1,5 @@
-import type {ExecuteQueryResponse} from '../../../types/api/query';
+import type {TClusterInfoV2, TStorageStats} from '../../../types/api/cluster';
+import type {ExecuteQueryResponse, KeyValueRow} from '../../../types/api/query';
import {parseQueryAPIExecuteResponse} from '../../../utils/query';
import type {ClusterGroupsStats} from './types';
@@ -31,13 +32,10 @@ const getDiskType = (rawTypeString: string) => {
return diskType;
};
-export const parseGroupsStatsQueryResponse = (
- data: ExecuteQueryResponse<'modern'>,
-): ClusterGroupsStats => {
- const parsedData = parseQueryAPIExecuteResponse(data).result;
+function getGroupStats(data?: KeyValueRow[] | TStorageStats[]) {
const result: ClusterGroupsStats = {};
- parsedData?.forEach((stats) => {
+ data?.forEach((stats) => {
const {
PDiskFilter,
ErasureSpecies: erasure,
@@ -85,8 +83,19 @@ export const parseGroupsStatsQueryResponse = (
});
return result;
+}
+
+export const parseGroupsStatsQueryResponse = (
+ data: ExecuteQueryResponse<'modern'>,
+): ClusterGroupsStats => {
+ const parsedData = parseQueryAPIExecuteResponse(data).result;
+ return getGroupStats(parsedData);
};
+export function getGroupStatsFromClusterInfo(info: TClusterInfoV2) {
+ return getGroupStats(info.StorageStats);
+}
+
export function normalizeDomain(domain?: string) {
if (!domain) {
return undefined;
diff --git a/src/styles/mixins.scss b/src/styles/mixins.scss
index ca142099f7..653c1afd05 100644
--- a/src/styles/mixins.scss
+++ b/src/styles/mixins.scss
@@ -323,3 +323,48 @@
}
}
}
+
+@mixin entity-state-colors {
+ --entity-state-border-color: var(--g-color-base-misc-heavy);
+ --entity-state-background-color: var(--g-color-base-misc-light);
+ --entity-state-fill-color: var(--g-color-base-misc-medium);
+ --entity-state-font-color: var(--g-color-text-primary);
+
+ &_green {
+ --entity-state-font-color: var(--g-color-text-positive);
+ --entity-state-border-color: var(--g-color-base-positive-heavy);
+ --entity-state-background-color: var(--g-color-base-positive-light);
+ --entity-state-fill-color: var(--g-color-base-positive-medium);
+ }
+
+ &_blue {
+ --entity-state-font-color: var(--g-color-text-info);
+ --entity-state-border-color: var(--g-color-base-info-heavy);
+ --entity-state-background-color: var(--g-color-base-info-light);
+ --entity-state-fill-color: var(--g-color-base-info-medium);
+ }
+
+ &_yellow {
+ --entity-state-font-color: var(--g-color-text-warning);
+ --entity-state-border-color: var(--g-color-base-warning-heavy);
+ --entity-state-background-color: var(--g-color-base-yellow-light);
+ --entity-state-fill-color: var(--g-color-base-yellow-medium);
+ }
+
+ &_orange {
+ --entity-state-font-color: var(--g-color-text-warning-heavy);
+ --entity-state-border-color: var(--ydb-color-status-orange);
+ --entity-state-background-color: var(--g-color-base-warning-light);
+ --entity-state-fill-color: var(--g-color-base-warning-medium);
+ }
+ &_red {
+ --entity-state-font-color: var(--g-color-text-danger);
+ --entity-state-border-color: var(--g-color-base-danger-heavy);
+ --entity-state-background-color: var(--g-color-base-danger-light);
+ --entity-state-fill-color: var(--g-color-base-danger-medium);
+ }
+ &__grey {
+ --entity-state-font-color: var(--g-color-text-secondary);
+ --entity-state-border-color: var(--g-color-line-generic-hover);
+ }
+}
diff --git a/src/types/api/cluster.ts b/src/types/api/cluster.ts
index 3c4acb4e27..04c53ba7cc 100644
--- a/src/types/api/cluster.ts
+++ b/src/types/api/cluster.ts
@@ -7,7 +7,7 @@ import type {TTraceCheck, TTraceView} from './trace';
*
* source: https://github.com/ydb-platform/ydb/blob/main/ydb/core/viewer/protos/viewer.proto
*/
-export interface TClusterInfo {
+export interface TClusterInfoV1 {
Name?: string;
Overall?: EFlag;
NodesTotal?: number;
@@ -42,3 +42,39 @@ export interface TClusterInfo {
TraceView?: TTraceView;
TraceCheck?: TTraceCheck;
}
+
+export interface TStorageStats {
+ PDiskFilter?: string;
+ ErasureSpecies?: string;
+ CurrentAvailableSize?: string;
+ /** uint64 */
+ CurrentAllocatedSize?: string;
+ CurrentGroupsCreated?: number;
+ AvailableGroupsToCreate?: number;
+}
+
+export interface TClusterInfoV2 extends TClusterInfoV1 {
+ MapDataCenters?: {
+ [key: string]: number;
+ };
+ MapNodeStates?: Partial>;
+ /** value is uint64 */
+ MapStorageTotal?: {
+ [key: string]: string;
+ };
+ /** value is uint64 */
+ MapStorageUsed?: {
+ [key: string]: string;
+ };
+ MapVersions?: {
+ [key: string]: number;
+ };
+ StorageStats?: TStorageStats[];
+ Version?: number;
+}
+
+export type TClusterInfo = TClusterInfoV1 | TClusterInfoV2;
+
+export function isClusterInfoV2(info?: TClusterInfo): info is TClusterInfoV2 {
+ return info ? 'Version' in info && info.Version === 2 : false;
+}
diff --git a/src/types/api/meta.ts b/src/types/api/meta.ts
index bf213bf6fe..ee7d946dc2 100644
--- a/src/types/api/meta.ts
+++ b/src/types/api/meta.ts
@@ -47,9 +47,9 @@ export interface MetaGeneralClusterInfo extends MetaBaseClusterInfo {
}
// In case of error in viewer /cluster request mvp return error field instead of cluster data
-export interface MetaViewerClusterInfo extends TClusterInfo {
+export type MetaViewerClusterInfo = TClusterInfo & {
error?: string;
-}
+};
export interface MetaClusterVersion {
version: string;
diff --git a/src/utils/versions/parseNodesToVersionsValues.ts b/src/utils/versions/parseNodesToVersionsValues.ts
index 6e6661d44f..e33eb208d7 100644
--- a/src/utils/versions/parseNodesToVersionsValues.ts
+++ b/src/utils/versions/parseNodesToVersionsValues.ts
@@ -32,13 +32,15 @@ export const parseNodesToVersionsValues = (
export function parseNodeGroupsToVersionsValues(
groups: NodesGroup[],
versionsToColor?: VersionToColorMap,
+ total?: number,
) {
+ const normalizedTotal = total ?? groups.reduce((acc, group) => acc + group.count, 0);
return groups.map((group) => {
return {
title: group.name,
version: group.name,
color: versionsToColor?.get(group.name),
- value: group.count,
+ value: (group.count / normalizedTotal) * 100,
};
});
}