diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts index 10e08c3586..34ebe3ff41 100644 --- a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts +++ b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts @@ -98,9 +98,9 @@ export const CDC_STREAM_PAGES = [overview, consumers, partitions, nodes, tablets export const TOPIC_PAGES = [overview, consumers, partitions, nodes, tablets, describe]; export const EXTERNAL_DATA_SOURCE_PAGES = [overview, describe]; -export const EXTERNAL_TABLE_PAGES = [overview, describe]; +export const EXTERNAL_TABLE_PAGES = [overview, schema, describe]; -export const VIEW_PAGES = [overview, describe]; +export const VIEW_PAGES = [overview, schema, describe]; // verbose mapping to guarantee correct tabs for new path types // TS will error when a new type is added but not mapped here diff --git a/src/containers/Tenant/ObjectSummary/ObjectSummary.scss b/src/containers/Tenant/ObjectSummary/ObjectSummary.scss index 46ca8e7b72..01439d7db2 100644 --- a/src/containers/Tenant/ObjectSummary/ObjectSummary.scss +++ b/src/containers/Tenant/ObjectSummary/ObjectSummary.scss @@ -1,6 +1,6 @@ @import '../../../styles/mixins.scss'; -.object-summary { +.ydb-object-summary { position: relative; display: flex; @@ -64,14 +64,13 @@ padding: 8px 12px 16px; } - &__tab { - margin-right: 40px; + &__tabs-inner { + --g-tabs-border-width: 0; + box-shadow: inset 0 -1px 0 0 var(--g-color-line-generic); + } + &__tab { text-decoration: none; - - &:first-letter { - text-transform: uppercase; - } } &__info { diff --git a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx index 9d57bb697b..9f6fe3a491 100644 --- a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx +++ b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx @@ -1,7 +1,7 @@ import React from 'react'; import {HelpPopover} from '@gravity-ui/components'; -import {Tabs} from '@gravity-ui/uikit'; +import {Flex, Tabs} from '@gravity-ui/uikit'; import qs from 'qs'; import {Link, useLocation} from 'react-router-dom'; import {StringParam, useQueryParam} from 'use-query-params'; @@ -12,14 +12,12 @@ import {toFormattedSize} from '../../../components/FormattedBytes/utils'; import {InfoViewer} from '../../../components/InfoViewer/InfoViewer'; 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, {createExternalUILink, createHref} from '../../../routes'; import {useGetSchemaQuery} from '../../../store/reducers/schema/schema'; 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 {cn} from '../../../utils/cn'; import { DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED, DEFAULT_SIZE_TENANT_SUMMARY_KEY, @@ -32,10 +30,8 @@ import { import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; import {Acl} from '../Acl/Acl'; import {EntityTitle} from '../EntityTitle/EntityTitle'; -import {SchemaTree} from '../Schema/SchemaTree/SchemaTree'; import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {TENANT_INFO_TABS, TENANT_SCHEMA_TAB, TenantTabsGroups} from '../TenantPages'; -import i18n from '../i18n'; import {getSummaryControls} from '../utils/controls'; import { PaneVisibilityActionTypes, @@ -44,12 +40,14 @@ import { } from '../utils/paneVisibilityToggleHelpers'; import {isIndexTableType, isTableType} from '../utils/schema'; +import {ObjectTree} from './ObjectTree'; +import {SchemaActions} from './SchemaActions'; +import i18n from './i18n'; +import {b} from './shared'; import {transformPath} from './transformPath'; import './ObjectSummary.scss'; -const b = cn('object-summary'); - const getTenantCommonInfoState = () => { const collapsed = Boolean(localStorage.getItem(DEFAULT_IS_TENANT_COMMON_INFO_COLLAPSED)); @@ -113,24 +111,31 @@ export function ObjectSummary({ return (
- { - const path = createHref(routes.tenant, undefined, { - ...queryParams, - name: tenantName, - [TenantTabsGroups.summaryTab]: id, - }); - return ( - - {node} - - ); - }} - allowNotSelected - /> + + { + const path = createHref(routes.tenant, undefined, { + ...queryParams, + name: tenantName, + [TenantTabsGroups.summaryTab]: id, + }); + return ( + + {node} + + ); + }} + allowNotSelected + /> + {summaryTab === TENANT_SUMMARY_TABS_IDS.schema && } +
); }; @@ -143,21 +148,21 @@ export function ObjectSummary({ const overview: InfoViewerItem[] = []; - overview.push({label: i18n('summary.type'), value: PathType?.replace(/^EPathType/, '')}); + overview.push({label: i18n('field_type'), value: PathType?.replace(/^EPathType/, '')}); if (PathSubType !== EPathSubType.EPathSubTypeEmpty) { overview.push({ - label: i18n('summary.subtype'), + label: i18n('field_subtype'), value: PathSubType?.replace(/^EPathSubType/, ''), }); } - overview.push({label: i18n('summary.id'), value: PathId}); + overview.push({label: i18n('field_id'), value: PathId}); - overview.push({label: i18n('summary.version'), value: PathVersion}); + overview.push({label: i18n('field_version'), value: PathVersion}); overview.push({ - label: i18n('summary.created'), + label: i18n('field_created'), value: formatDateTime(CreateStep), }); @@ -168,11 +173,11 @@ export function ObjectSummary({ overview.push( { - label: i18n('summary.data-size'), + label: i18n('field_data-size'), value: toFormattedSize(DataSize), }, { - label: i18n('summary.row-count'), + label: i18n('field_row-count'), value: formatNumber(RowCount), }, ); @@ -196,11 +201,11 @@ export function ObjectSummary({ return [ { - label: i18n('summary.paths'), + label: i18n('field_paths'), value: paths, }, { - label: i18n('summary.shards'), + label: i18n('field_shards'), value: shards, }, ]; @@ -211,7 +216,7 @@ export function ObjectSummary({ [EPathType.EPathTypeDir]: undefined, [EPathType.EPathTypeTable]: () => [ { - label: i18n('summary.partitions'), + label: i18n('field_partitions'), value: PathDescription?.TablePartitions?.length, }, ], @@ -220,13 +225,13 @@ export function ObjectSummary({ [EPathType.EPathTypeExtSubDomain]: getDatabaseOverview, [EPathType.EPathTypeColumnStore]: () => [ { - label: i18n('summary.partitions'), + label: i18n('field_partitions'), value: PathDescription?.ColumnStoreDescription?.ColumnShards?.length, }, ], [EPathType.EPathTypeColumnTable]: () => [ { - label: i18n('summary.partitions'), + label: i18n('field_partitions'), value: PathDescription?.ColumnTableDescription?.Sharding?.ColumnShards?.length, }, ], @@ -235,11 +240,11 @@ export function ObjectSummary({ return [ { - label: i18n('summary.mode'), + label: i18n('field_mode'), value: Mode?.replace(/^ECdcStreamMode/, ''), }, { - label: i18n('summary.format'), + label: i18n('field_format'), value: Format?.replace(/^ECdcStreamFormat/, ''), }, ]; @@ -250,11 +255,11 @@ export function ObjectSummary({ return [ { - label: i18n('summary.partitions'), + label: i18n('field_partitions'), value: pqGroup?.Partitions?.length, }, { - label: i18n('summary.retention'), + label: i18n('field_retention'), value: value && formatSecondsToHours(value), }, ]; @@ -271,9 +276,9 @@ export function ObjectSummary({ const dataSourceName = DataSourcePath?.match(/([^/]*)\/*$/)?.[1] || ''; return [ - {label: i18n('summary.source-type'), value: SourceType}, + {label: i18n('field_source-type'), value: SourceType}, { - label: i18n('summary.data-source'), + label: i18n('field_data-source'), value: DataSourcePath && ( @@ -284,7 +289,7 @@ export function ObjectSummary({ }, [EPathType.EPathTypeExternalDataSource]: () => [ { - label: i18n('summary.source-type'), + label: i18n('field_source-type'), value: PathDescription?.ExternalDataSourceDescription?.SourceType, }, ], @@ -298,7 +303,7 @@ export function ObjectSummary({ return [ { - label: i18n('summary.state'), + label: i18n('field_state'), value: , }, ]; @@ -351,7 +356,7 @@ export function ObjectSummary({ - - - ); - } - - return ( -
-
{i18n('summary.navigation')}
-
- {pathData ? ( - - ) : null} -
-
- ); -} diff --git a/src/containers/Tenant/ObjectSummary/ObjectTree.tsx b/src/containers/Tenant/ObjectSummary/ObjectTree.tsx new file mode 100644 index 0000000000..e1a10b87a4 --- /dev/null +++ b/src/containers/Tenant/ObjectSummary/ObjectTree.tsx @@ -0,0 +1,51 @@ +import {StringParam, useQueryParam} from 'use-query-params'; + +import {Loader} from '../../../components/Loader'; +import {useGetSchemaQuery} from '../../../store/reducers/schema/schema'; +import {SchemaTree} from '../Schema/SchemaTree/SchemaTree'; + +import i18n from './i18n'; +import {b} from './shared'; + +interface ObjectTreeProps { + tenantName: string; + path?: string; +} + +export function ObjectTree({tenantName, path}: ObjectTreeProps) { + const {data: tenantData = {}, isLoading} = useGetSchemaQuery({ + path: tenantName, + database: tenantName, + }); + const pathData = tenantData?.PathDescription?.Self; + + const [, setCurrentPath] = useQueryParam('schema', StringParam); + + if (!pathData && isLoading) { + // If Loader isn't wrapped with div, SplitPane doesn't calculate panes height correctly + return ( +
+ +
+ ); + } + + return ( +
+
{i18n('title_navigation')}
+
+ {pathData ? ( + + ) : null} +
+
+ ); +} diff --git a/src/containers/Tenant/ObjectSummary/SchemaActions.tsx b/src/containers/Tenant/ObjectSummary/SchemaActions.tsx new file mode 100644 index 0000000000..9c96ef1def --- /dev/null +++ b/src/containers/Tenant/ObjectSummary/SchemaActions.tsx @@ -0,0 +1,37 @@ +import {Button, Icon} from '@gravity-ui/uikit'; + +import { + TENANT_DIAGNOSTICS_TABS_IDS, + TENANT_PAGES_IDS, +} from '../../../store/reducers/tenant/constants'; +import {setDiagnosticsTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; +import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks'; + +import i18n from './i18n'; + +import ArrowRightFromSquareIcon from '@gravity-ui/icons/svgs/arrow-right-from-square.svg'; + +export function SchemaActions() { + const dispatch = useTypedDispatch(); + const {diagnosticsTab, tenantPage} = useTypedSelector((state) => state.tenant); + const diagnosticsSchemaActive = + tenantPage === TENANT_PAGES_IDS.diagnostics && + diagnosticsTab === TENANT_DIAGNOSTICS_TABS_IDS.schema; + + return ( +
+ {!diagnosticsSchemaActive && ( + + )} +
+ ); +} diff --git a/src/containers/Tenant/ObjectSummary/i18n/en.json b/src/containers/Tenant/ObjectSummary/i18n/en.json new file mode 100644 index 0000000000..7dcb277cf8 --- /dev/null +++ b/src/containers/Tenant/ObjectSummary/i18n/en.json @@ -0,0 +1,21 @@ +{ + "title_navigation": "Navigation", + "field_source-type": "Source Type", + "field_data-source": "Data Source", + "action_copySchemaPath": "Copy schema path", + "action_openInDiagnostics": "Open in Diagnostics", + "field_type": "Type", + "field_subtype": "SubType", + "field_id": "Id", + "field_version": "Version", + "field_created": "Created", + "field_data-size": "Data size", + "field_row-count": "Row count", + "field_partitions": "Partitions count", + "field_paths": "Paths", + "field_shards": "Shards", + "field_state": "State", + "field_mode": "Mode", + "field_format": "Format", + "field_retention": "Retention" +} diff --git a/src/containers/Tenant/ObjectSummary/i18n/index.ts b/src/containers/Tenant/ObjectSummary/i18n/index.ts new file mode 100644 index 0000000000..cfa8a570f2 --- /dev/null +++ b/src/containers/Tenant/ObjectSummary/i18n/index.ts @@ -0,0 +1,7 @@ +import {registerKeysets} from '../../../../utils/i18n'; + +import en from './en.json'; + +const COMPONENT = 'ydb-object-summary'; + +export default registerKeysets(COMPONENT, {en}); diff --git a/src/containers/Tenant/ObjectSummary/shared.ts b/src/containers/Tenant/ObjectSummary/shared.ts new file mode 100644 index 0000000000..f05505f22c --- /dev/null +++ b/src/containers/Tenant/ObjectSummary/shared.ts @@ -0,0 +1,3 @@ +import {cn} from '../../../utils/cn'; + +export const b = cn('ydb-object-summary'); diff --git a/src/containers/Tenant/utils/schema.ts b/src/containers/Tenant/utils/schema.ts index 42de559110..4257c3e1c3 100644 --- a/src/containers/Tenant/utils/schema.ts +++ b/src/containers/Tenant/utils/schema.ts @@ -120,6 +120,7 @@ const pathTypeToIsTable: Record = { [EPathType.EPathTypeReplication]: false, }; +//if add entity with tableType, make sure that Schema is available in Diagnostics section export const isTableType = (pathType?: EPathType) => (pathType && pathTypeToIsTable[pathType]) ?? false; diff --git a/tests/suites/tenant/summary/ObjectSummary.ts b/tests/suites/tenant/summary/ObjectSummary.ts index 364798f390..2d5dbed234 100644 --- a/tests/suites/tenant/summary/ObjectSummary.ts +++ b/tests/suites/tenant/summary/ObjectSummary.ts @@ -16,9 +16,9 @@ export class ObjectSummary { private primaryKeys: Locator; constructor(page: Page) { - this.tree = page.locator('.object-summary__tree'); + this.tree = page.locator('.ydb-object-summary__tree'); this.treeRows = page.locator('.ydb-tree-view'); - this.tabs = page.locator('.object-summary__tabs'); + this.tabs = page.locator('.ydb-object-summary__tabs'); this.schemaViewer = page.locator('.schema-viewer'); this.primaryKeys = page.locator('.schema-viewer__keys_type_primary'); } @@ -66,7 +66,7 @@ export class ObjectSummary { } async clickTab(tabName: ObjectSummaryTab): Promise { - const tab = this.tabs.locator(`.object-summary__tab:has-text("${tabName}")`); + const tab = this.tabs.locator(`.ydb-object-summary__tab:has-text("${tabName}")`); await tab.click(); }