import React from 'react'; import {CirclePlus} from '@gravity-ui/icons'; import type {Column, SortOrder} from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; import type {LabelProps} from '@gravity-ui/uikit'; import {Button, Flex, Icon, Label} from '@gravity-ui/uikit'; import {useHistory} from 'react-router-dom'; import {EntitiesCount} from '../../components/EntitiesCount'; import {ResponseError} from '../../components/Errors/ResponseError'; import {PoolsGraph} from '../../components/PoolsGraph/PoolsGraph'; import {ProblemFilter} from '../../components/ProblemFilter/ProblemFilter'; import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable'; import {Search} from '../../components/Search'; import {TableColumnSetup} from '../../components/TableColumnSetup/TableColumnSetup'; import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout'; import {TenantNameWrapper} from '../../components/TenantNameWrapper/TenantNameWrapper'; import {getTenantLink} from '../../components/TenantNameWrapper/utils'; import {useEmMetaAvailable} from '../../store/reducers/capabilities/hooks'; import {useClusterBaseInfo} from '../../store/reducers/cluster/cluster'; import { filterTenantsByDomain, filterTenantsByProblems, filterTenantsBySearch, } from '../../store/reducers/tenants/filters'; import {tenantsApi} from '../../store/reducers/tenants/tenants'; import type {PreparedTenant} from '../../store/reducers/tenants/types'; import type {AdditionalTenantsProps} from '../../types/additionalProps'; import {State} from '../../types/api/tenant'; import {uiFactory} from '../../uiFactory/uiFactory'; import {formatBytes} from '../../utils/bytesParsers'; import {cn} from '../../utils/cn'; import { BRAND_BUTTON_CLASS, DEFAULT_TABLE_SETTINGS, EMPTY_DATA_PLACEHOLDER, } from '../../utils/constants'; import { formatCPU, formatNumber, formatStorageValuesToGb, } from '../../utils/dataFormatters/dataFormatters'; import {useAutoRefreshInterval} from '../../utils/hooks'; import {useSelectedColumns} from '../../utils/hooks/useSelectedColumns'; import {getIllustration} from '../../utils/illustrations'; import {isNumeric} from '../../utils/utils'; import type {DatabaseStatusClickHandler} from './DatabaseDrawerHealthcheck'; import {DatabaseDrawerHealthcheck} from './DatabaseDrawerHealthcheck'; import { DATABASES_COLUMNS_IDS, DATABASES_COLUMNS_TITLES, DATABASES_DEFAULT_COLUMNS, DATABASES_REQUIRED_COLUMNS, DATABASES_SELECTED_COLUMNS_KEY, } from './constants'; import i18n from './i18n'; import {useTenantsQueryParams} from './useTenantsQueryParams'; import './Tenants.scss'; const b = cn('tenants'); const DATABASES_COLUMNS_WIDTH_LS_KEY = 'databasesTableColumnsWidth'; function formatDatabaseState(state?: State): string { if (!state) { return EMPTY_DATA_PLACEHOLDER; } // Map specific state values to user-friendly display names switch (state) { case State.STATE_UNSPECIFIED: return 'Unspecified'; case State.PENDING_RESOURCES: return 'Pending'; default: // For other states, use capitalized version (first letter uppercase, rest lowercase) return state.charAt(0).toUpperCase() + state.slice(1).toLowerCase(); } } function databaseStateToLabelTheme(state?: State): LabelProps['theme'] { switch (state) { case 'CREATING': case 'CONFIGURING': { return 'info'; } case 'RUNNING': { return 'success'; } case 'REMOVING': { return 'danger'; } case 'PENDING_RESOURCES': { return 'warning'; } case 'STATE_UNSPECIFIED': default: { return 'unknown'; } } } interface TenantsTableProps { clusterName?: string; environmentName?: string; isMetaDatabasesAvailable?: boolean; scrollContainerRef: React.RefObject; additionalTenantsProps?: AdditionalTenantsProps; showDomainDatabase?: boolean; showWithProblemsFilter?: boolean; showPoolsColumn?: boolean; } interface TenantsTableContentProps extends TenantsTableProps { onStatusClick: DatabaseStatusClickHandler; } const TenantsTableContent = ({ clusterName, environmentName, isMetaDatabasesAvailable, scrollContainerRef, additionalTenantsProps, showDomainDatabase, showWithProblemsFilter, showPoolsColumn, onStatusClick, }: TenantsTableContentProps) => { const SuccessImage = getIllustration('SuccessOperation'); const history = useHistory(); const [autoRefreshInterval] = useAutoRefreshInterval(); const {currentData, isFetching, error} = tenantsApi.useGetTenantsInfoQuery( {clusterName, environmentName, isMetaDatabasesAvailable}, {pollingInterval: autoRefreshInterval}, ); // Track sort state for scroll dependencies const [sortParams, setSortParams] = React.useState(); const isCreateDBAvailable = useEmMetaAvailable() && uiFactory.onCreateDB !== undefined; const {domain: domainRoot, settings, isResolved: isClusterInfoResolved} = useClusterBaseInfo(); const loading = (isFetching && currentData === undefined) || !isClusterInfoResolved; const {search, withProblems, handleSearchChange, handleWithProblemsChange} = useTenantsQueryParams(); // We should apply domain filter before other filters // It is done to ensure proper entities count // It should be 8/8 instead of 8/9 when no filters applied but show domain setting is off const tenants = React.useMemo(() => { const rawTenants = currentData ?? []; return filterTenantsByDomain(rawTenants, showDomainDatabase); }, [currentData, showDomainDatabase]); const filteredTenants = React.useMemo(() => { let filteredByProblems = tenants; if (showWithProblemsFilter) { filteredByProblems = filterTenantsByProblems(tenants, withProblems); } const filteredBySearch = filterTenantsBySearch(filteredByProblems, search); return filteredBySearch; }, [tenants, withProblems, search, showWithProblemsFilter]); const useMetaProxySetting = settings?.use_meta_proxy; const useDatabaseIdSetting = settings?.use_database_id; const tenantLinks = React.useMemo( () => new Map( tenants.map((tenant) => [ tenant, getTenantLink({ tenant, additionalTenantsProps, externalLink: Boolean(environmentName), useDatabaseId: uiFactory.useDatabaseId, isMetaDatabasesAvailable, settings: { use_meta_proxy: useMetaProxySetting, use_database_id: useDatabaseIdSetting, }, }), ]), ), [ tenants, additionalTenantsProps, environmentName, isMetaDatabasesAvailable, useMetaProxySetting, useDatabaseIdSetting, ], ); const openTenant = React.useCallback( (tenant: PreparedTenant) => { const link = tenantLinks.get(tenant); if (!isClusterInfoResolved || !link) { return; } if (link.isExternalLink) { window.open(link.href, '_blank', 'noopener,noreferrer'); return; } history.push(link.href); }, [tenantLinks, history, isClusterInfoResolved], ); const renderCreateDBButton = () => { const buttonAvailable = isCreateDBAvailable && clusterName; if (buttonAvailable && !loading) { return ( ); } return null; }; const renderControls = () => { return ( {showWithProblemsFilter ? ( ) : null} ); }; const rawDatabasesColumns = React.useMemo(() => { const columns: Column[] = [ { name: DATABASES_COLUMNS_IDS.Name, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.Name], render: ({row}) => ( ), width: 440, resizeMinWidth: 200, sortable: true, defaultOrder: DataTable.DESCENDING, }, { name: DATABASES_COLUMNS_IDS.Type, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.Type], width: 200, resizeMinWidth: 150, render: ({row}) => { if (row.Type !== 'Serverless') { return row.Type; } return (
{row.Type}
); }, }, { name: DATABASES_COLUMNS_IDS.State, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.State], width: 150, render: ({row}) => ( ), }, { name: DATABASES_COLUMNS_IDS.CPU, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.CPU], width: 80, render: ({row}) => { // Don't show values below 0.01 when formatted if (row.cpu && row.cpu > 10_000) { return formatCPU(row.cpu); } return EMPTY_DATA_PLACEHOLDER; }, align: DataTable.RIGHT, defaultOrder: DataTable.DESCENDING, }, { name: DATABASES_COLUMNS_IDS.Memory, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.Memory], width: 120, render: ({row}) => row.memory ? formatStorageValuesToGb(row.memory) : EMPTY_DATA_PLACEHOLDER, align: DataTable.RIGHT, defaultOrder: DataTable.DESCENDING, }, { name: DATABASES_COLUMNS_IDS.Storage, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.Storage], width: 120, render: ({row}) => row.storage ? formatStorageValuesToGb(row.storage) : EMPTY_DATA_PLACEHOLDER, align: DataTable.RIGHT, defaultOrder: DataTable.DESCENDING, }, ]; columns.push({ name: DATABASES_COLUMNS_IDS.Network, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.Network], width: 120, align: DataTable.RIGHT, defaultOrder: DataTable.DESCENDING, sortAccessor: ({NetworkWriteThroughput = 0}) => Number(NetworkWriteThroughput), render: ({row}) => { const {NetworkWriteThroughput} = row; if (!isNumeric(NetworkWriteThroughput)) { return EMPTY_DATA_PLACEHOLDER; } return formatBytes({ value: NetworkWriteThroughput, size: 'mb', withSpeedLabel: true, precision: 2, }); }, }); columns.push( { name: DATABASES_COLUMNS_IDS.NodesCount, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.NodesCount], width: 100, render: ({row}) => row.nodesCount ? formatNumber(row.nodesCount) : EMPTY_DATA_PLACEHOLDER, align: DataTable.RIGHT, defaultOrder: DataTable.DESCENDING, }, { name: DATABASES_COLUMNS_IDS.GroupsCount, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.GroupsCount], width: 100, render: ({row}) => row.groupsCount ? formatNumber(row.groupsCount) : EMPTY_DATA_PLACEHOLDER, align: DataTable.RIGHT, defaultOrder: DataTable.DESCENDING, }, ); if (showPoolsColumn) { columns.push({ name: DATABASES_COLUMNS_IDS.PoolStats, header: DATABASES_COLUMNS_TITLES[DATABASES_COLUMNS_IDS.PoolStats], width: 100, resizeMinWidth: 60, sortAccessor: ({PoolStats = []}) => PoolStats.reduce((acc, item) => acc + (item.Usage || 0), 0), defaultOrder: DataTable.DESCENDING, align: DataTable.LEFT, render: ({row}) => , }); } return columns; }, [ clusterName, additionalTenantsProps, environmentName, handleSearchChange, onStatusClick, showPoolsColumn, tenantLinks, ]); const {columnsToShow, columnsToSelect, setColumns} = useSelectedColumns( rawDatabasesColumns, DATABASES_SELECTED_COLUMNS_KEY, DATABASES_COLUMNS_TITLES, DATABASES_DEFAULT_COLUMNS, DATABASES_REQUIRED_COLUMNS, ); const renderTable = () => { if (filteredTenants.length === 0 && withProblems) { return ; } return (
JSON.stringify([tenant.Cluster, tenant.Id ?? tenant.Name]) } getKeyboardRowLabel={(tenant) => tenant.controlPlaneName || tenant.Name || tenant.Id || EMPTY_DATA_PLACEHOLDER } columnsWidthLSKey={DATABASES_COLUMNS_WIDTH_LS_KEY} data={filteredTenants} columns={columnsToShow} settings={DEFAULT_TABLE_SETTINGS} emptyDataMessage={i18n('no-databases')} onSortChange={setSortParams} />
); }; const renderColumnSetup = () => { return ( ); }; const renderExtraControls = () => { return ( {renderCreateDBButton()} {renderColumnSetup()} ); }; return (
{renderControls()} {error ? : null}
{currentData ? renderTable() : null}
); }; export function TenantsTable(props: TenantsTableProps) { return ( {(onStatusClick) => } ); }