import React from 'react'; import type {DefinitionListItemProps} from '@gravity-ui/uikit'; import { ClipboardButton, DefinitionList, Flex, HelpMark, Tab, TabList, TabProvider, } from '@gravity-ui/uikit'; import qs from 'qs'; import {useLocation} from 'react-router-dom'; import {AsyncReplicationState} from '../../../components/AsyncReplicationState'; import {toFormattedSize} from '../../../components/FormattedBytes/utils'; import {InternalLink} from '../../../components/InternalLink'; import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon'; import SplitPane from '../../../components/SplitPane'; import {createExternalUILink, getTenantPath} from '../../../routes'; import {useClusterWithProxy} from '../../../store/reducers/cluster/cluster'; import {overviewApi} from '../../../store/reducers/overview/overview'; import {TENANT_SUMMARY_TABS_IDS} from '../../../store/reducers/tenant/constants'; import {setSummaryTab} from '../../../store/reducers/tenant/tenant'; import {EPathSubType, EPathType} from '../../../types/api/schema'; import { DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED, DEFAULT_SIZE_TENANT_SUMMARY_KEY, } from '../../../utils/constants'; import { formatDateTime, formatNumber, formatSecondsToHours, } from '../../../utils/dataFormatters/dataFormatters'; import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; import {useSetting} from '../../../utils/hooks/useSetting'; import {prepareSystemViewType} from '../../../utils/schema'; import {EntityTitle} from '../EntityTitle/EntityTitle'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {useCurrentSchema} from '../TenantContext'; import {useTenantPage} from '../TenantNavigation/useTenantNavigation'; import {TENANT_INFO_TABS, TENANT_SCHEMA_TAB, TenantTabsGroups} from '../TenantPages'; import {ROW_COUNT_NOTE} from '../constants'; import tenantKeyset from '../i18n'; import {useTenantQueryParams} from '../useTenantQueryParams'; import {getSummaryControls} from '../utils/controls'; import { PaneVisibilityActionTypes, PaneVisibilityToggleButtons, paneVisibilityToggleReducer, } from '../utils/paneVisibilityToggleHelpers'; import {isTableType} from '../utils/schema'; import {useNavigationV2Enabled} from '../utils/useNavigationV2Enabled'; import {ObjectTree} from './ObjectTree'; import {SchemaActions} from './SchemaActions'; import {RefreshTreeButton} from './SchemaTree/RefreshTreeButton'; import i18n from './i18n'; import {b} from './shared'; import {getPathTypeName, isDomain, transformPath} from './transformPath'; import './ObjectSummary.scss'; interface ObjectSummaryProps { onCollapseSummary: VoidFunction; onExpandSummary: VoidFunction; isCollapsed: boolean; type: EPathType | undefined; } const INFO_PANEL_MIN_SIZES = [200, 52]; const HIDDEN_INFO_PANEL_SIZES = [100, 0]; const HIDDEN_INFO_PANEL_MIN_SIZES = [0, 0]; export function ObjectSummary({ onCollapseSummary, onExpandSummary, isCollapsed, type, }: ObjectSummaryProps) { const {path, database, databaseFullPath} = useCurrentSchema(); const useMetaProxy = useClusterWithProxy(); const dispatch = useTypedDispatch(); const {handleSchemaChange} = useTenantQueryParams(); const [isCommonInfoCollapsed, setIsCommonInfoCollapsed] = useSetting( DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED, false, ); const isV2Navigation = useNavigationV2Enabled(); const showLegacyDatabaseInfo = !isV2Navigation && path === databaseFullPath; const [commonInfoVisibilityState, dispatchCommonInfoVisibilityState] = React.useReducer( paneVisibilityToggleReducer, undefined, () => ({ triggerExpand: false, triggerCollapse: false, collapsed: isCommonInfoCollapsed, }), ); const {summaryTab = TENANT_SUMMARY_TABS_IDS.overview} = useTypedSelector( (state) => state.tenant, ); const tabsItems = React.useMemo(() => { const availableTabs = isTableType(type) ? [...TENANT_INFO_TABS, ...TENANT_SCHEMA_TAB] : TENANT_INFO_TABS; if (showLegacyDatabaseInfo) { return availableTabs; } return availableTabs.filter(({id}) => id !== TENANT_SUMMARY_TABS_IDS.overview); }, [showLegacyDatabaseInfo, type]); const showInfoPanel = tabsItems.length > 0; const {handleTenantPageChange} = useTenantPage(); const location = useLocation(); const queryParams = qs.parse(location.search, { ignoreQueryPrefix: true, }); const {currentData: currentObjectData} = overviewApi.useGetOverviewQuery({ path, database, databaseFullPath, useMetaProxy, }); const currentSchemaData = currentObjectData?.PathDescription?.Self; React.useEffect(() => { const isSummaryTabAvailable = tabsItems.some(({id}) => id === summaryTab); if (!isSummaryTabAvailable && tabsItems[0]) { dispatch(setSummaryTab(tabsItems[0].id)); } }, [dispatch, summaryTab, tabsItems]); const renderTabs = () => { return (
{tabsItems.map(({id, title}) => { const tabPath = getTenantPath({ ...queryParams, [TenantTabsGroups.summaryTab]: id, }); return ( {title} ); })} {summaryTab === TENANT_SUMMARY_TABS_IDS.schema && }
); }; const renderObjectOverview = () => { if (!currentSchemaData) { return undefined; } const {CreateStep, PathType, PathSubType, PathId, PathVersion} = currentSchemaData; const overview: { name: string; content?: React.ReactNode; note?: DefinitionListItemProps['note']; }[] = []; const normalizedType = getPathTypeName(path, PathType); overview.push({name: tenantKeyset('field_type'), content: normalizedType}); if (PathSubType !== EPathSubType.EPathSubTypeEmpty) { overview.push({ name: tenantKeyset('field_subtype'), content: PathSubType?.replace(/^EPathSubType/, ''), }); } overview.push({name: tenantKeyset('field_id'), content: PathId}); overview.push({name: tenantKeyset('field_version'), content: PathVersion}); if (Number(CreateStep)) { overview.push({ name: tenantKeyset('field_created'), content: formatDateTime(CreateStep), }); } const {PathDescription} = currentObjectData; if (PathDescription?.TableStats) { const {DataSize, RowCount} = PathDescription.TableStats; overview.push( { name: tenantKeyset('field_data-size'), content: toFormattedSize(DataSize), }, { name: tenantKeyset('field_row-count'), content: formatNumber(RowCount), note: ROW_COUNT_NOTE, }, ); } const getDatabaseOverview = () => { const {PathsInside, ShardsInside, PathsLimit, ShardsLimit} = PathDescription?.DomainDescription ?? {}; let paths = formatNumber(PathsInside); let shards = formatNumber(ShardsInside); if (paths && PathsLimit) { paths = `${paths} / ${formatNumber(PathsLimit)}`; } if (shards && ShardsLimit) { shards = `${shards} / ${formatNumber(ShardsLimit)}`; } return [ { name: tenantKeyset('field_paths'), content: paths, }, { name: tenantKeyset('field_shards'), content: shards, }, ]; }; const getPathTypeOverview: Record< EPathType, | (() => { name: string; content?: React.ReactNode; note?: DefinitionListItemProps['note']; }[]) | undefined > = { [EPathType.EPathTypeInvalid]: undefined, [EPathType.EPathTypeDir]: undefined, [EPathType.EPathTypeResourcePool]: undefined, [EPathType.EPathTypeSecret]: undefined, [EPathType.EPathTypeTable]: () => [ { name: tenantKeyset('field_partitions-count'), content: PathDescription?.TablePartitions?.length, }, ], [EPathType.EPathTypeSysView]: () => [ { name: tenantKeyset('field_system-view-type'), content: prepareSystemViewType(PathDescription?.SysViewDescription?.Type), }, ], [EPathType.EPathTypeSubDomain]: isV2Navigation ? undefined : getDatabaseOverview, [EPathType.EPathTypeTableIndex]: undefined, [EPathType.EPathTypeExtSubDomain]: isV2Navigation ? undefined : getDatabaseOverview, [EPathType.EPathTypeColumnStore]: () => [ { name: tenantKeyset('field_partitions-count'), content: PathDescription?.ColumnStoreDescription?.ColumnShards?.length, }, ], [EPathType.EPathTypeColumnTable]: () => [ { name: tenantKeyset('field_partitions-count'), content: PathDescription?.ColumnTableDescription?.Sharding?.ColumnShards?.length, }, ], [EPathType.EPathTypeCdcStream]: () => { const {Mode, Format} = PathDescription?.CdcStreamDescription || {}; return [ { name: tenantKeyset('field_mode'), content: Mode?.replace(/^ECdcStreamMode/, ''), }, { name: tenantKeyset('field_format'), content: Format?.replace(/^ECdcStreamFormat/, ''), }, ]; }, [EPathType.EPathTypePersQueueGroup]: () => { const pqGroup = PathDescription?.PersQueueGroup; const value = pqGroup?.PQTabletConfig?.PartitionConfig?.LifetimeSeconds; return [ { name: tenantKeyset('field_partitions-count'), content: pqGroup?.Partitions?.length, }, { name: tenantKeyset('field_retention'), content: value && formatSecondsToHours(value), }, ]; }, [EPathType.EPathTypeExternalTable]: () => { const pathToDataSource = createExternalUILink({ ...queryParams, schema: PathDescription?.ExternalTableDescription?.DataSourcePath, }); const {SourceType, DataSourcePath} = PathDescription?.ExternalTableDescription || {}; const dataSourceName = DataSourcePath?.match(/([^/]*)\/*$/)?.[1] || ''; return [ {name: tenantKeyset('field_source-type'), content: SourceType}, { name: tenantKeyset('field_data-source'), content: DataSourcePath && ( ), }, ]; }, [EPathType.EPathTypeExternalDataSource]: () => [ { name: tenantKeyset('field_source-type'), content: PathDescription?.ExternalDataSourceDescription?.SourceType, }, ], [EPathType.EPathTypeView]: undefined, [EPathType.EPathTypeReplication]: () => { const state = PathDescription?.ReplicationDescription?.State; if (!state) { return []; } return [ { name: tenantKeyset('field_state'), content: , }, ]; }, [EPathType.EPathTypeTransfer]: () => { const state = PathDescription?.ReplicationDescription?.State; if (!state) { return []; } return [ { name: tenantKeyset('field_state'), content: , }, ]; }, [EPathType.EPathTypeStreamingQuery]: undefined, }; const pathTypeOverview = (PathType && getPathTypeOverview[PathType]?.()) || []; overview.push(...pathTypeOverview); // filter all empty values in according this requirement // https://github.com/ydb-platform/ydb-embedded-ui/issues/906 const listItems = overview .filter((i) => i.content) .map((el) => ({ ...el, content:
{el.content}
, })); return (
{listItems.map((item) => ( {item.content} ))}
); }; const renderTabContent = () => { switch (summaryTab) { case TENANT_SUMMARY_TABS_IDS.schema: { return ( ); } case TENANT_SUMMARY_TABS_IDS.overview: { return showLegacyDatabaseInfo ? renderObjectOverview() : null; } default: { return null; } } }; const onCollapseInfoHandler = () => { setIsCommonInfoCollapsed(true); dispatchCommonInfoVisibilityState(PaneVisibilityActionTypes.triggerCollapse); }; const onExpandInfoHandler = () => { setIsCommonInfoCollapsed(false); dispatchCommonInfoVisibilityState(PaneVisibilityActionTypes.triggerExpand); }; const onSplitStartDragAdditional = () => { setIsCommonInfoCollapsed(false); dispatchCommonInfoVisibilityState(PaneVisibilityActionTypes.clear); }; const relativePath = transformPath(path, databaseFullPath); const renderCommonInfoControls = () => { const showPreview = isTableType(type); return ( {showPreview && getSummaryControls( dispatch, {setActivePath: handleSchemaChange, setTenantPage: handleTenantPageChange}, 'm', )(path, 'preview')} ); }; const renderEntityTypeBadge = () => { const {Status, Reason} = currentObjectData ?? {}; if (type) { let label = type.replace('EPathType', ''); if (isDomain(path, type)) { label = 'domain'; } return
{label}
; } let message; if (Status && Reason) { message = `${Status}: ${Reason}`; } return (
); }; const renderContent = () => { return (
{showInfoPanel ? (
{renderEntityTypeBadge()}
{relativePath}
{renderCommonInfoControls()}
{renderTabs()}
{renderTabContent()}
) : null}
{!isCollapsed && }
); }; return renderContent(); }