diff --git a/.eslintrc b/.eslintrc index 5f81ea8606..9dc0df0ea7 100644 --- a/.eslintrc +++ b/.eslintrc @@ -7,7 +7,10 @@ "root": true, "overrides": [ { - "files": ["config-overrides.js", "commitlint.config.js"], + "files": [ + "config-overrides.js", + "commitlint.config.js" + ], "env": { "node": true, }, @@ -17,10 +20,20 @@ "project": "./tsconfig.json", }, "rules": { - "import/consistent-type-specifier-style": ["error", "prefer-top-level"], + "import/consistent-type-specifier-style": [ + "error", + "prefer-top-level" + ], "@typescript-eslint/consistent-type-imports": [ "error", - {"prefer": "type-imports", "fixStyle": "separate-type-imports"}, + { + "prefer": "type-imports", + "fixStyle": "separate-type-imports" + }, + ], + "curly": [ + "error", + "all" ], }, -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 24767d9790..4e6afaad9f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ yarn-debug.log* yarn-error.log* .env + + +embedded-ui.tar.bz2 \ No newline at end of file diff --git a/src/components/InfoViewer/schemaOverview/CDCStreamOverview.tsx b/src/components/InfoViewer/schemaOverview/CDCStreamOverview.tsx deleted file mode 100644 index 01202fcf04..0000000000 --- a/src/components/InfoViewer/schemaOverview/CDCStreamOverview.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; - -import type {InfoViewerItem} from '..'; -import {InfoViewer, formatObject} from '..'; -import type {TEvDescribeSchemeResult} from '../../../types/api/schema'; -import {formatCdcStreamItem, formatCommonItem} from '../formatters'; - -interface CDCStreamOverviewProps { - data?: TEvDescribeSchemeResult; -} - -export const CDCStreamOverview = ({data}: CDCStreamOverviewProps) => { - if (!data) { - return
No CDC Stream data
; - } - - const TableIndex = data.PathDescription?.CdcStreamDescription; - const info: Array = []; - - info.push(formatCommonItem('PathType', data.PathDescription?.Self?.PathType)); - info.push(formatCommonItem('CreateStep', data.PathDescription?.Self?.CreateStep)); - - const {Mode, Format} = TableIndex || {}; - info.push(...formatObject(formatCdcStreamItem, {Mode, Format})); - - return ( - - {info.length ? : 'Empty'} - - ); -}; diff --git a/src/components/InfoViewer/schemaOverview/PersQueueGroupOverview.tsx b/src/components/InfoViewer/schemaOverview/PersQueueGroupOverview.tsx deleted file mode 100644 index a18939afe4..0000000000 --- a/src/components/InfoViewer/schemaOverview/PersQueueGroupOverview.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; - -import type {InfoViewerItem} from '..'; -import {InfoViewer} from '..'; -import type {TEvDescribeSchemeResult} from '../../../types/api/schema'; -import {formatCommonItem, formatPQGroupItem} from '../formatters'; - -interface PersQueueGroupOverviewProps { - data?: TEvDescribeSchemeResult; -} - -export const PersQueueGroupOverview = ({data}: PersQueueGroupOverviewProps) => { - if (!data) { - return
No PersQueueGroup data
; - } - - const pqGroup = data.PathDescription?.PersQueueGroup; - const info: Array = []; - - info.push(formatCommonItem('PathType', data.PathDescription?.Self?.PathType)); - info.push(formatCommonItem('CreateStep', data.PathDescription?.Self?.CreateStep)); - - //@ts-expect-error - info.push(formatPQGroupItem('Partitions', pqGroup?.Partitions || [])); - info.push( - //@ts-expect-error - formatPQGroupItem( - 'PQTabletConfig', - pqGroup?.PQTabletConfig || {PartitionConfig: {LifetimeSeconds: 0}}, - ), - ); - - return ( - - {info.length ? : 'Empty'} - - ); -}; diff --git a/src/components/InfoViewer/schemaOverview/index.ts b/src/components/InfoViewer/schemaOverview/index.ts deleted file mode 100644 index 5da37b7dd2..0000000000 --- a/src/components/InfoViewer/schemaOverview/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './CDCStreamOverview'; -export * from './PersQueueGroupOverview'; diff --git a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx index c9a7c33314..ca7c84d572 100644 --- a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx +++ b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx @@ -11,13 +11,12 @@ import {StringParam, useQueryParam} from 'use-query-params'; import {AsyncReplicationState} from '../../../components/AsyncReplicationState'; import {ClipboardButton} from '../../../components/ClipboardButton'; import InfoViewer from '../../../components/InfoViewer/InfoViewer'; -import { - CDCStreamOverview, - PersQueueGroupOverview, -} from '../../../components/InfoViewer/schemaOverview'; +import type {InfoViewerItem} from '../../../components/InfoViewer/InfoViewer'; +import {LinkWithIcon} from '../../../components/LinkWithIcon/LinkWithIcon'; import {Loader} from '../../../components/Loader'; import SplitPane from '../../../components/SplitPane'; -import routes, {createHref} from '../../../routes'; +import {getEntityName} from '../../../containers/Tenant/utils'; +import routes, {createExternalUILink, createHref} from '../../../routes'; import {schemaApi, setShowPreview} from '../../../store/reducers/schema/schema'; import { TENANT_PAGES_IDS, @@ -25,18 +24,15 @@ import { TENANT_SUMMARY_TABS_IDS, } from '../../../store/reducers/tenant/constants'; import {setQueryTab, setSummaryTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; -import type {EPathSubType} from '../../../types/api/schema'; -import {EPathType} from '../../../types/api/schema'; +import {EPathSubType, EPathType} from '../../../types/api/schema'; import {cn} from '../../../utils/cn'; import { DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED, DEFAULT_SIZE_TENANT_SUMMARY_KEY, } from '../../../utils/constants'; -import {formatDateTime} from '../../../utils/dataFormatters/dataFormatters'; +import {formatDateTime, formatSecondsToHours} from '../../../utils/dataFormatters/dataFormatters'; import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; import {Acl} from '../Acl/Acl'; -import {ExternalDataSourceSummary} from '../Info/ExternalDataSource/ExternalDataSource'; -import {ExternalTableSummary} from '../Info/ExternalTable/ExternalTable'; import {SchemaTree} from '../Schema/SchemaTree/SchemaTree'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {TENANT_INFO_TABS, TENANT_SCHEMA_TAB, TenantTabsGroups} from '../TenantPages'; @@ -137,65 +133,148 @@ export function ObjectSummary({ }; const renderObjectOverview = () => { - const startTimeInMilliseconds = Number(currentSchemaData?.CreateStep); - const createdAt = startTimeInMilliseconds - ? formatDateTime(startTimeInMilliseconds) - : 'unknown'; - const createdAtLabel = 'Created At'; - - // verbose mapping to guarantee a correct render for new path types - // TS will error when a new type is added but not mapped here - const pathTypeToComponent: Record React.ReactNode) | undefined> = { + if (!currentSchemaData) { + return undefined; + } + const {CreateStep, PathType, PathSubType, PathId, PathVersion} = currentSchemaData; + + const overview: InfoViewerItem[] = []; + + overview.push({label: i18n('summary.type'), value: PathType?.replace(/^EPathType/, '')}); + + if (PathSubType !== EPathSubType.EPathSubTypeEmpty) { + overview.push({ + label: i18n('summary.subtype'), + value: PathSubType?.replace(/^EPathSubType/, ''), + }); + } + + overview.push({label: i18n('summary.id'), value: PathId}); + + overview.push({label: i18n('summary.version'), value: PathVersion}); + + overview.push({ + label: i18n('summary.created'), + value: formatDateTime(CreateStep, ''), + }); + + const {PathDescription} = currentObjectData; + const title = getEntityName(PathDescription); + + const getPathTypeOverview: Record InfoViewerItem[]) | undefined> = { [EPathType.EPathTypeInvalid]: undefined, [EPathType.EPathTypeDir]: undefined, - [EPathType.EPathTypeTable]: undefined, + [EPathType.EPathTypeTable]: () => [ + { + label: i18n('summary.partitions'), + value: PathDescription?.TablePartitions?.length, + }, + ], [EPathType.EPathTypeSubDomain]: undefined, [EPathType.EPathTypeTableIndex]: undefined, - [EPathType.EPathTypeExtSubDomain]: undefined, - [EPathType.EPathTypeColumnStore]: undefined, - [EPathType.EPathTypeColumnTable]: undefined, - [EPathType.EPathTypeCdcStream]: () => , - [EPathType.EPathTypePersQueueGroup]: () => ( - - ), - [EPathType.EPathTypeExternalTable]: () => ( - - ), - [EPathType.EPathTypeExternalDataSource]: () => ( - - ), + [EPathType.EPathTypeExtSubDomain]: () => [ + { + label: i18n('summary.paths'), + value: PathDescription?.DomainDescription?.PathsInside, + }, + { + label: i18n('summary.shards'), + value: PathDescription?.DomainDescription?.ShardsInside, + }, + ], + [EPathType.EPathTypeColumnStore]: () => [ + { + label: i18n('summary.partitions'), + value: PathDescription?.ColumnStoreDescription?.ColumnShards?.length, + }, + ], + [EPathType.EPathTypeColumnTable]: () => [ + { + label: i18n('summary.partitions'), + value: PathDescription?.ColumnTableDescription?.Sharding?.ColumnShards?.length, + }, + ], + [EPathType.EPathTypeCdcStream]: () => { + const {Mode, Format} = PathDescription?.CdcStreamDescription || {}; + + return [ + { + label: i18n('summary.mode'), + value: Mode?.replace(/^ECdcStreamMode/, ''), + }, + { + label: i18n('summary.format'), + value: Format?.replace(/^ECdcStreamFormat/, ''), + }, + ]; + }, + [EPathType.EPathTypePersQueueGroup]: () => { + const pqGroup = PathDescription?.PersQueueGroup; + const value = pqGroup?.PQTabletConfig?.PartitionConfig?.LifetimeSeconds; + + return [ + { + label: i18n('summary.partitions'), + value: pqGroup?.Partitions?.length, + }, + { + label: i18n('summary.retention'), + value: value && formatSecondsToHours(value), + }, + ]; + }, + [EPathType.EPathTypeExternalTable]: () => { + const pathToDataSource = createExternalUILink({ + ...queryParams, + schema: PathDescription?.ExternalTableDescription?.DataSourcePath, + }); + + const {SourceType, DataSourcePath} = + PathDescription?.ExternalTableDescription || {}; + + const dataSourceName = DataSourcePath?.match(/([^/]*)\/*$/)?.[1] || ''; + + return [ + {label: i18n('summary.source-type'), value: SourceType}, + { + label: i18n('summary.data-source'), + value: DataSourcePath && ( + + + + ), + }, + ]; + }, + [EPathType.EPathTypeExternalDataSource]: () => [ + { + label: i18n('summary.source-type'), + value: PathDescription?.ExternalDataSourceDescription?.SourceType, + }, + ], [EPathType.EPathTypeView]: undefined, - [EPathType.EPathTypeReplication]: () => ( - - ), - }, - ]} - /> - ), + [EPathType.EPathTypeReplication]: () => { + const state = PathDescription?.ReplicationDescription?.State; + + if (!state) { + return []; + } + + return [ + { + label: i18n('summary.state'), + value: , + }, + ]; + }, }; - let component = - currentSchemaData?.PathType && pathTypeToComponent[currentSchemaData.PathType]?.(); - - if (!component) { - component = ; - } + const pathTypeOverview = (PathType && getPathTypeOverview[PathType]?.()) || []; + overview.push(...pathTypeOverview); - return component; + // filter all empty values in according this requirement + // https://github.com/ydb-platform/ydb-embedded-ui/issues/906 + return i.value)} />; }; const renderTabContent = () => { diff --git a/src/containers/Tenant/i18n/en.json b/src/containers/Tenant/i18n/en.json index 3be5439b82..defb9ee44a 100644 --- a/src/containers/Tenant/i18n/en.json +++ b/src/containers/Tenant/i18n/en.json @@ -1,19 +1,28 @@ { "page.title": "Database", - "pages.query": "Query", "pages.diagnostics": "Diagnostics", - "acl.owner": "Owner", "acl.empty": "No Acl data", - "summary.navigation": "Navigation", "summary.showPreview": "Show preview", + "summary.source-type": "Source Type", + "summary.data-source": "Data Source", "summary.copySchemaPath": "Copy schema path", - + "summary.type": "Type", + "summary.subtype": "SubType", + "summary.id": "Id", + "summary.version": "Version", + "summary.created": "Created", + "summary.partitions": "Partitions count", + "summary.paths": "Paths", + "summary.shards": "Shards", + "summary.state": "State", + "summary.mode": "Mode", + "summary.format": "Format", + "summary.retention": "Retention", "actions.copied": "The path is copied to the clipboard", "actions.notCopied": "Couldn’t copy the path", - "actions.copyPath": "Copy path", "actions.openPreview": "Open preview", "actions.createTable": "Create table...", diff --git a/src/utils/dataFormatters/dataFormatters.ts b/src/utils/dataFormatters/dataFormatters.ts index 063506ce74..212bd85631 100644 --- a/src/utils/dataFormatters/dataFormatters.ts +++ b/src/utils/dataFormatters/dataFormatters.ts @@ -6,7 +6,7 @@ import { getSizeWithSignificantDigits, } from '../bytesParsers/formatBytes'; import type {BytesSizes} from '../bytesParsers/formatBytes'; -import {DAY_IN_SECONDS, GIGABYTE} from '../constants'; +import {DAY_IN_SECONDS, GIGABYTE, HOUR_IN_SECONDS} from '../constants'; import {configuredNumeral} from '../numeral'; import {isNumeric} from '../utils'; @@ -99,6 +99,11 @@ export const formatNumber = (number?: unknown) => { return configuredNumeral(number).format('0,0.[00000]'); }; +export const formatSecondsToHours = (seconds: number) => { + const hours = (seconds / HOUR_IN_SECONDS).toFixed(2); + return `${formatNumber(hours)} hours`; +}; + export const roundToPrecision = (value: number | string, precision = 0) => { let [digits] = String(value).split('.'); if (Number(value) < 1) { @@ -134,14 +139,14 @@ export const formatCPUWithLabel = (value?: number) => { return `${localizedCores} ${i18n('format-cpu.cores', {count: cores})}`; }; -export const formatDateTime = (value?: number | string) => { +export const formatDateTime = (value?: number | string, defaultValue = 'N/A') => { if (!isNumeric(value)) { return ''; } const formattedData = dateTimeParse(Number(value))?.format('YYYY-MM-DD HH:mm'); - return Number(value) > 0 && formattedData ? formattedData : 'N/A'; + return Number(value) > 0 && formattedData ? formattedData : defaultValue; }; export const calcUptimeInSeconds = (milliseconds: number | string) => {