From e2ac8290440e5247d9eed5e07d8c8cf9d611c810 Mon Sep 17 00:00:00 2001 From: mufazalov Date: Thu, 26 Jun 2025 18:12:46 +0300 Subject: [PATCH 1/3] feat(Clusters): redesign table --- src/components/Search/Search.tsx | 10 +- src/containers/Clusters/Clusters.scss | 78 ++-------- src/containers/Clusters/Clusters.tsx | 18 ++- src/containers/Clusters/columns.tsx | 214 ++++++++++++++++---------- src/containers/Clusters/constants.ts | 8 +- src/containers/Clusters/i18n/en.json | 7 +- 6 files changed, 163 insertions(+), 172 deletions(-) diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx index 3892f94cd..4923baedc 100644 --- a/src/components/Search/Search.tsx +++ b/src/components/Search/Search.tsx @@ -1,5 +1,7 @@ import React from 'react'; +import type {TextInputProps} from '@gravity-ui/uikit'; + import {cn} from '../../utils/cn'; import {DebouncedInput} from '../DebouncedInput/DebouncedTextInput'; @@ -7,13 +9,11 @@ import './Search.scss'; const b = cn('ydb-search'); -interface SearchProps { +interface SearchProps extends Omit { onChange: (value: string) => void; value?: string; width?: React.CSSProperties['width']; - className?: string; debounce?: number; - placeholder?: string; inputRef?: React.RefObject; } @@ -23,8 +23,8 @@ export const Search = ({ width, className, debounce, - placeholder, inputRef, + ...props }: SearchProps) => { return ( ); }; diff --git a/src/containers/Clusters/Clusters.scss b/src/containers/Clusters/Clusters.scss index 2e12a1349..72d21aa07 100644 --- a/src/containers/Clusters/Clusters.scss +++ b/src/containers/Clusters/Clusters.scss @@ -17,37 +17,6 @@ &__autorefresh { margin-left: auto; } - &__cluster-status { - width: 18px; - height: 18px; - margin-right: 8px; - - border-radius: 3px; - - & span { - display: flex; - align-items: center; - } - - &_type_green { - background-color: var(--ydb-color-status-green); - } - &_type_yellow { - background-color: var(--ydb-color-status-yellow); - } - &_type_blue { - background-color: var(--ydb-color-status-blue); - } - &_type_red { - background: var(--ydb-color-status-red); - } - &_type_grey { - background: var(--ydb-color-status-grey); - } - &_type_orange { - background: var(--ydb-color-status-orange); - } - } &__cluster-name { white-space: normal; text-decoration: none; @@ -71,29 +40,17 @@ margin-right: 15px; &_wide { - width: 300px; + width: 320px; } } &__empty-cell { color: var(--g-color-text-secondary); } - &__tooltip-content { - word-break: break-all; - } & .g-progress__item { transition: none; } - &__text { - color: var(--g-color-text-primary); - @include mixins.body-2-typography(); - - &::first-letter { - color: var(--g-color-text-danger); - } - } - &__description { max-width: 200px; @@ -113,36 +70,25 @@ } &__table { + --data-table-cell-align: top; + --data-table-cell-vertical-padding: var(--g-spacing-3); @include mixins.freeze-nth-column(1); } - &__balancer-cell { - display: flex; - flex-direction: row; - align-items: center; - } - - &__balancer-text { - display: inline-block; - overflow: hidden; - - max-width: 92%; - margin-right: 5px; - - text-overflow: ellipsis; - overflow-wrap: break-word !important; - } - &__balancer-icon { - display: flex; - align-items: center; + &__balancer-copy-icon { + color: var(--g-color-text-secondary); } + &__search-icon { + margin: 0 var(--g-spacing-1); - &__error { - margin-left: 15px; - @include mixins.body-2-typography(); + color: var(--g-color-text-secondary); } &__remove-cluster { color: var(--ydb-color-status-red); } + + &__progress { + --g-progress-filled-background-color: var(--ydb-color-status-green); + } } diff --git a/src/containers/Clusters/Clusters.tsx b/src/containers/Clusters/Clusters.tsx index 6f6ed8821..52c85629e 100644 --- a/src/containers/Clusters/Clusters.tsx +++ b/src/containers/Clusters/Clusters.tsx @@ -1,7 +1,8 @@ import React from 'react'; +import {Magnifier} from '@gravity-ui/icons'; import DataTable from '@gravity-ui/react-data-table'; -import {Flex, Select, TableColumnSetup, Text} from '@gravity-ui/uikit'; +import {Flex, Icon, Select, TableColumnSetup, Text} from '@gravity-ui/uikit'; import {Helmet} from 'react-helmet-async'; import {AutoRefreshControl} from '../../components/AutoRefreshControl/AutoRefreshControl'; @@ -129,12 +130,7 @@ export function Clusters() { const renderPageTitle = () => { return ( - - {i18n('page_title')} - - {clusters?.length} - - + {i18n('page_title')} ); @@ -152,6 +148,7 @@ export function Clusters() {
} onChange={changeClusterName} value={clusterName} /> @@ -206,7 +203,12 @@ export function Clusters() { />
- {query.isError ? : null} + {clusters?.length ? ( + + {i18n('clusters-count', {count: filteredClusters?.length})} + + ) : null} + {query.isError ? : null} {query.isLoading ? : null} {query.fulfilledTimeStamp ? (
diff --git a/src/containers/Clusters/columns.tsx b/src/containers/Clusters/columns.tsx index 291d5adbf..4dbb24810 100644 --- a/src/containers/Clusters/columns.tsx +++ b/src/containers/Clusters/columns.tsx @@ -1,6 +1,5 @@ import React from 'react'; -import {HelpPopover} from '@gravity-ui/components'; import {Pencil, TrashBin} from '@gravity-ui/icons'; import DataTable from '@gravity-ui/react-data-table'; import type {Column} from '@gravity-ui/react-data-table'; @@ -10,14 +9,16 @@ import { DropdownMenu, Link as ExternalLink, Flex, + Label, Progress, + Text, } from '@gravity-ui/uikit'; -import {ProgressViewer} from '../../components/ProgressViewer/ProgressViewer'; -import {UserCard} from '../../components/User/User'; +import {EntityStatus} from '../../components/EntityStatusNew/EntityStatus'; import type {PreparedCluster} from '../../store/reducers/clusters/types'; +import {EFlag} from '../../types/api/enums'; import {uiFactory} from '../../uiFactory/uiFactory'; -import {formatStorageValuesToTb} from '../../utils/dataFormatters/dataFormatters'; +import {formatNumber, formatStorageValuesToTb} from '../../utils/dataFormatters/dataFormatters'; import {createDeveloperUIMonitoringPageHref} from '../../utils/developerUI/developerUI'; import {getCleanBalancerValue} from '../../utils/parseBalancer'; import {clusterTabsIds, getClusterPath} from '../Cluster/utils'; @@ -39,8 +40,9 @@ function getTitleColumn({isEditClusterAvailable, isDeleteClusterAvailable}: Clus return { name: COLUMNS_NAMES.TITLE, header: COLUMNS_TITLES[COLUMNS_NAMES.TITLE], - width: 230, + width: 320, defaultOrder: DataTable.ASCENDING, + sortAccessor: (row) => row.title || row.name, render: ({row}) => { const { name: clusterName, @@ -55,6 +57,8 @@ function getTitleColumn({isEditClusterAvailable, isDeleteClusterAvailable}: Clus const clusterStatus = row.cluster?.Overall; + const cleanedBalancer = row.balancer ? getCleanBalancerValue(row.balancer) : null; + const renderActions = () => { const menuItems: (DropdownMenuItem | DropdownMenuItem[])[] = []; @@ -87,41 +91,63 @@ function getTitleColumn({isEditClusterAvailable, isDeleteClusterAvailable}: Clus return ( ); }; + const renderName = () => { + return ( +
+ {row.title || row.name} +
+ ); + }; + + const renderStatus = () => { + if (clusterStatus) { + return ; + } + return ( + + ); + }; + + const renderBalancer = () => { + if (!cleanedBalancer) { + return null; + } + + return ( + + + {cleanedBalancer} + + + + ); + }; + return ( - - - {clusterStatus ? ( - -
- - ) : ( -
- - {row.cluster?.error || i18n('tooltip_no-cluster-data')} - - } - offset={{left: 0}} - /> -
- )} -
- {row.title || row.name} -
+ + + {renderName()} + + {renderStatus()} + {renderActions()} + - {renderActions()} + {renderBalancer()} ); }, @@ -193,6 +219,21 @@ const CLUSTERS_COLUMNS: Column[] = [ ); }, }, + { + name: COLUMNS_NAMES.STATUS, + header: COLUMNS_TITLES[COLUMNS_NAMES.STATUS], + width: 180, + sortable: true, + render: ({row}) => { + return ; + }, + }, + { + name: COLUMNS_NAMES.SERVICE, + header: COLUMNS_TITLES[COLUMNS_NAMES.SERVICE], + width: 100, + sortable: true, + }, { name: COLUMNS_NAMES.DC, header: COLUMNS_TITLES[COLUMNS_NAMES.DC], @@ -206,21 +247,10 @@ const CLUSTERS_COLUMNS: Column[] = [ ); }, }, - { - name: COLUMNS_NAMES.SERVICE, - header: COLUMNS_TITLES[COLUMNS_NAMES.SERVICE], - width: 100, - sortable: true, - }, - { - name: COLUMNS_NAMES.STATUS, - header: COLUMNS_TITLES[COLUMNS_NAMES.STATUS], - width: 150, - sortable: true, - }, { name: COLUMNS_NAMES.NODES, header: COLUMNS_TITLES[COLUMNS_NAMES.NODES], + width: 170, resizeMinWidth: 170, defaultOrder: DataTable.DESCENDING, sortAccessor: ({cluster = {}}) => { @@ -235,16 +265,26 @@ const CLUSTERS_COLUMNS: Column[] = [ return EMPTY_CELL; } - return ; + return ( + + ); }, }, { name: COLUMNS_NAMES.LOAD, header: COLUMNS_TITLES[COLUMNS_NAMES.LOAD], + width: 170, resizeMinWidth: 170, defaultOrder: DataTable.DESCENDING, sortAccessor: ({cluster}) => { - return cluster?.NumberOfCpus; + return cluster?.NumberOfCpus || 0; }, render: ({row}) => { const { @@ -259,17 +299,25 @@ const CLUSTERS_COLUMNS: Column[] = [ } return ( - + ); }, }, { name: COLUMNS_NAMES.STORAGE, header: COLUMNS_TITLES[COLUMNS_NAMES.STORAGE], + width: 170, resizeMinWidth: 170, defaultOrder: DataTable.DESCENDING, sortAccessor: ({cluster}) => { - return Number(cluster?.StorageTotal); + return Number(cluster?.StorageTotal) || 0; }, render: ({row}) => { const {StorageUsed = 0, StorageTotal = 0, Overall} = row.cluster || {}; @@ -278,11 +326,19 @@ const CLUSTERS_COLUMNS: Column[] = [ return EMPTY_CELL; } + const [valueString, capacityString] = formatStorageValuesToTb( + Number(StorageUsed), + Number(StorageTotal), + ); + return ( - ); }, @@ -311,23 +367,11 @@ const CLUSTERS_COLUMNS: Column[] = [ return Number(row.cluster?.Tenants) || EMPTY_CELL; }, }, - { - name: COLUMNS_NAMES.OWNER, - header: COLUMNS_TITLES[COLUMNS_NAMES.OWNER], - sortable: false, - width: 120, - render: ({row}) => { - const logins = row.owner?.split(', '); - return logins?.length - ? logins.map((login) => ) - : EMPTY_CELL; - }, - }, { name: COLUMNS_NAMES.DESCRIPTION, header: COLUMNS_TITLES[COLUMNS_NAMES.DESCRIPTION], sortable: false, - width: 150, + width: 200, render: ({row}) => { return row.description ? (
{row.description}
@@ -336,27 +380,29 @@ const CLUSTERS_COLUMNS: Column[] = [ ); }, }, - { - name: COLUMNS_NAMES.BALANCER, - header: COLUMNS_TITLES[COLUMNS_NAMES.BALANCER], - sortable: false, - width: 290, - render: ({row}) => { - if (!row.balancer) { - return EMPTY_CELL; - } - - const cleanedValue = getCleanBalancerValue(row.balancer); - return ( -
-
{cleanedValue}
- -
- ); - }, - }, ]; export function getClustersColumns(params: ClustersColumnsParams) { return [getTitleColumn(params), ...CLUSTERS_COLUMNS]; } + +function ClustersTableProgressBar({ + value, + capacity, + description, +}: { + value: number; + capacity: number; + description?: string; +}) { + const usage = (value / capacity) * 100; + + return ( + +
+ +
+ {description} +
+ ); +} diff --git a/src/containers/Clusters/constants.ts b/src/containers/Clusters/constants.ts index 58149dd42..70a0e517b 100644 --- a/src/containers/Clusters/constants.ts +++ b/src/containers/Clusters/constants.ts @@ -11,9 +11,7 @@ export const COLUMNS_NAMES = { STORAGE: 'storage', HOSTS: 'hosts', TENANTS: 'tenants', - OWNER: 'owner', DESCRIPTION: 'description', - BALANCER: 'balancer', } as const; export const DEFAULT_COLUMNS = [ @@ -26,8 +24,6 @@ export const DEFAULT_COLUMNS = [ COLUMNS_NAMES.STORAGE, COLUMNS_NAMES.HOSTS, COLUMNS_NAMES.TENANTS, - COLUMNS_NAMES.OWNER, - COLUMNS_NAMES.BALANCER, ]; export const COLUMNS_TITLES = { @@ -40,8 +36,6 @@ export const COLUMNS_TITLES = { [COLUMNS_NAMES.LOAD]: 'Load', [COLUMNS_NAMES.STORAGE]: 'Storage', [COLUMNS_NAMES.HOSTS]: 'Hosts', - [COLUMNS_NAMES.TENANTS]: 'Tenants', - [COLUMNS_NAMES.OWNER]: 'Owner', + [COLUMNS_NAMES.TENANTS]: 'Databases', [COLUMNS_NAMES.DESCRIPTION]: 'Description', - [COLUMNS_NAMES.BALANCER]: 'Balancer', } as const; diff --git a/src/containers/Clusters/i18n/en.json b/src/containers/Clusters/i18n/en.json index 49164c22b..5cfcc2781 100644 --- a/src/containers/Clusters/i18n/en.json +++ b/src/containers/Clusters/i18n/en.json @@ -3,7 +3,7 @@ "controls_service-select-label": "Service:", "controls_version-select-label": "Version:", - "controls_search-placeholder": "Cluster name, version, host", + "controls_search-placeholder": "Cluster name, version, host...", "controls_select-placeholder": "All", "statistics_clusters": "Clusters", @@ -15,7 +15,10 @@ "tooltip_no-cluster-data": "No cluster data", - "page_title": "Clusters", + "clusters-count": "{{count}} clusters", + "entities-count": "{{value}} of {{capacity}}", + + "page_title": "YDB Enterprise Manager", "edit-cluster": "Edit Cluster", "remove-cluster": "Remove Cluster" From 5aff66d3d83921adaad3f973d888e4cb09fc355e Mon Sep 17 00:00:00 2001 From: mufazalov Date: Fri, 27 Jun 2025 13:29:32 +0300 Subject: [PATCH 2/3] align buttons to cell top --- src/containers/Clusters/columns.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/containers/Clusters/columns.tsx b/src/containers/Clusters/columns.tsx index 4dbb24810..02b9fd187 100644 --- a/src/containers/Clusters/columns.tsx +++ b/src/containers/Clusters/columns.tsx @@ -140,7 +140,7 @@ function getTitleColumn({isEditClusterAvailable, isDeleteClusterAvailable}: Clus return ( - + {renderName()} {renderStatus()} From 4b4f77802fc4ab9ba135012f623d32aa8e501e4e Mon Sep 17 00:00:00 2001 From: mufazalov Date: Mon, 30 Jun 2025 14:51:15 +0300 Subject: [PATCH 3/3] fix bot comment, title in uiFactory --- src/containers/Clusters/Clusters.tsx | 2 +- src/containers/Clusters/columns.tsx | 2 +- src/containers/Clusters/i18n/en.json | 2 +- src/uiFactory/types.ts | 2 ++ 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/containers/Clusters/Clusters.tsx b/src/containers/Clusters/Clusters.tsx index 52c85629e..2cadf127d 100644 --- a/src/containers/Clusters/Clusters.tsx +++ b/src/containers/Clusters/Clusters.tsx @@ -130,7 +130,7 @@ export function Clusters() { const renderPageTitle = () => { return ( - {i18n('page_title')} + {uiFactory.clustersPageTitle ?? i18n('page_title')} ); diff --git a/src/containers/Clusters/columns.tsx b/src/containers/Clusters/columns.tsx index 02b9fd187..ec168f21c 100644 --- a/src/containers/Clusters/columns.tsx +++ b/src/containers/Clusters/columns.tsx @@ -395,7 +395,7 @@ function ClustersTableProgressBar({ capacity: number; description?: string; }) { - const usage = (value / capacity) * 100; + const usage = capacity ? (value / capacity) * 100 : 0; return ( diff --git a/src/containers/Clusters/i18n/en.json b/src/containers/Clusters/i18n/en.json index 5cfcc2781..2e2749eac 100644 --- a/src/containers/Clusters/i18n/en.json +++ b/src/containers/Clusters/i18n/en.json @@ -18,7 +18,7 @@ "clusters-count": "{{count}} clusters", "entities-count": "{{value}} of {{capacity}}", - "page_title": "YDB Enterprise Manager", + "page_title": "Clusters", "edit-cluster": "Edit Cluster", "remove-cluster": "Remove Cluster" diff --git a/src/uiFactory/types.ts b/src/uiFactory/types.ts index 11388b613..766a4ef69 100644 --- a/src/uiFactory/types.ts +++ b/src/uiFactory/types.ts @@ -23,6 +23,8 @@ export interface UIFactory { onEditCluster?: HandleEditCluster; onDeleteCluster?: HandleDeleteCluster; + clustersPageTitle?: string; + getLogsLink?: GetLogsLink; getMonitoringLink?: GetMonitoringLink; getMonitoringClusterLink?: GetMonitoringClusterLink;