diff --git a/README.md b/README.md index 6936722db3..7c7e3bdace 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,13 @@ Open http://localhost:8765 to view it in the browser. 1. Run on a machine with Docker installed: ``` - docker pull cr.yandex/yc/yandex-docker-local-ydb - docker run --hostname localhost -e YDB_ALLOW_ORIGIN="http://localhost:3000" -dp 2135:2135 -dp 8765:8765 cr.yandex/yc/yandex-docker-local-ydb + docker run --rm -ti --name ydb-local -h localhost \ + -p 8765:8765 \ + -e MON_PORT=8765 \ + cr.yandex/yc/yandex-docker-local-ydb:latest ``` 2. Run the frontend app in the development mode, via invoking `npm run dev` -3. Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.\ +3. Open [localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.\ You will also see any lint errors in the console. For API reference, open Swagger UI on http://localhost:8765/viewer/api/. @@ -37,7 +39,9 @@ For API reference, open Swagger UI on http://localhost:8765/viewer/api/. Image `cr.yandex/yc/yandex-docker-local-ydb` corresponds to `:latest` tag. It's the latest stable ydb version. -To test new features, you can use ydb version that is currently in testing mode with `cr.yandex/yc/yandex-docker-local-ydb:edge` image. Also you can set specific version like `cr.yandex/yc/yandex-docker-local-ydb:23.1` +To test new features, you can use ydb version that is currently in testing mode with `cr.yandex/yc/yandex-docker-local-ydb:edge` image +or use a build from `main` brunch with `ghcr.io/ydb-platform/local-ydb:trunk` image. +Also you can set specific version like `cr.yandex/yc/yandex-docker-local-ydb:23.1` ### Custom backend in dev mode diff --git a/src/containers/Tenant/Acl/Acl.scss b/src/containers/Tenant/Acl/Acl.scss index 3c59c58ab6..7104eaa276 100644 --- a/src/containers/Tenant/Acl/Acl.scss +++ b/src/containers/Tenant/Acl/Acl.scss @@ -5,17 +5,12 @@ overflow: auto; flex-grow: 1; - padding: 0 12px 16px; @include query-data-table; &__result { align-self: flex-start; } - &__message-container { - padding: 0 12px 16px; - } - &__owner-container { position: sticky; z-index: 2; diff --git a/src/containers/Tenant/Acl/Acl.tsx b/src/containers/Tenant/Acl/Acl.tsx index 3dafd055a1..c1304428e0 100644 --- a/src/containers/Tenant/Acl/Acl.tsx +++ b/src/containers/Tenant/Acl/Acl.tsx @@ -118,11 +118,11 @@ export const Acl = () => { } if (error) { - return ; + return ; } if (!loading && !acl && !owner) { - return
{i18n('acl.empty')}
; + return {i18n('acl.empty')}; } return ( diff --git a/src/containers/Tenant/Diagnostics/Diagnostics.tsx b/src/containers/Tenant/Diagnostics/Diagnostics.tsx index 962176b9dd..1b56b61dc4 100644 --- a/src/containers/Tenant/Diagnostics/Diagnostics.tsx +++ b/src/containers/Tenant/Diagnostics/Diagnostics.tsx @@ -19,6 +19,7 @@ import {Heatmap} from '../../Heatmap'; import {NodesWrapper} from '../../Nodes/NodesWrapper'; import {StorageWrapper} from '../../Storage/StorageWrapper'; import {Tablets} from '../../Tablets'; +import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer'; import {TenantTabsGroups} from '../TenantPages'; import {isDatabaseEntityType} from '../utils/schema'; @@ -103,6 +104,9 @@ function Diagnostics(props: DiagnosticsProps) { /> ); } + case TENANT_DIAGNOSTICS_TABS_IDS.schema: { + return ; + } case TENANT_DIAGNOSTICS_TABS_IDS.topQueries: { return ; } diff --git a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts index 8316c6780b..8eb5cc7536 100644 --- a/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts +++ b/src/containers/Tenant/Diagnostics/DiagnosticsPages.ts @@ -12,6 +12,11 @@ const overview = { title: 'Info', }; +const schema = { + id: TENANT_DIAGNOSTICS_TABS_IDS.schema, + title: 'Schema', +}; + const topQueries = { id: TENANT_DIAGNOSTICS_TABS_IDS.topQueries, title: 'Top queries', @@ -76,8 +81,8 @@ export const DATABASE_PAGES = [ describe, ]; -export const TABLE_PAGES = [overview, topShards, nodes, graph, tablets, hotKeys, describe]; -export const COLUMN_TABLE_PAGES = [overview, topShards, nodes, graph, tablets, describe]; +export const TABLE_PAGES = [overview, schema, topShards, nodes, graph, tablets, hotKeys, describe]; +export const COLUMN_TABLE_PAGES = [overview, schema, topShards, nodes, graph, tablets, describe]; export const DIR_PAGES = [overview, topShards, nodes, describe]; diff --git a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx index b7c21eb5b9..e6dfb9c653 100644 --- a/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx +++ b/src/containers/Tenant/ObjectSummary/ObjectSummary.tsx @@ -23,11 +23,7 @@ import { TENANT_SUMMARY_TABS_IDS, } from '../../../store/reducers/tenant/constants'; import {setQueryTab, setSummaryTab, setTenantPage} from '../../../store/reducers/tenant/tenant'; -import type { - EPathSubType, - TColumnDescription, - TColumnTableDescription, -} from '../../../types/api/schema'; +import type {EPathSubType} from '../../../types/api/schema'; import {EPathType} from '../../../types/api/schema'; import {cn} from '../../../utils/cn'; import { @@ -48,7 +44,7 @@ import { PaneVisibilityToggleButtons, paneVisibilityToggleReducerCreator, } from '../utils/paneVisibilityToggleHelpers'; -import {isColumnEntityType, isExternalTable, isIndexTable, isTableType} from '../utils/schema'; +import {isIndexTable, isTableType} from '../utils/schema'; import './ObjectSummary.scss'; @@ -64,29 +60,6 @@ const getTenantCommonInfoState = () => { }; }; -function prepareOlapTableSchema(tableSchema: TColumnTableDescription = {}) { - const {Name, Schema} = tableSchema; - - if (Schema) { - const {Columns, KeyColumnNames} = Schema; - const KeyColumnIds = KeyColumnNames?.map((name: string) => { - const column = Columns?.find((el) => el.Name === name); - return column?.Id; - }).filter((id): id is number => id !== undefined); - - return { - Columns, - KeyColumnNames, - Name, - KeyColumnIds, - }; - } - - return { - Name, - }; -} - interface ObjectSummaryProps { type?: EPathType; subType?: EPathSubType; @@ -112,7 +85,6 @@ export function ObjectSummary({ data, currentSchemaPath, currentSchema: currentItem = {}, - loading: loadingSchema, } = useTypedSelector((state) => state.schema); const {summaryTab = TENANT_SUMMARY_TABS_IDS.overview} = useTypedSelector( (state) => state.tenant, @@ -130,21 +102,6 @@ export function ObjectSummary({ const currentObjectData = currentSchemaPath ? data[currentSchemaPath] : undefined; const currentSchemaData = currentObjectData?.PathDescription?.Self; - let keyColumnIds: number[] | undefined; - let columns: TColumnDescription[] | undefined; - - if (isTableType(type) && isColumnEntityType(type)) { - const description = currentObjectData?.PathDescription?.ColumnTableDescription; - const columnTableSchema = prepareOlapTableSchema(description); - keyColumnIds = columnTableSchema.KeyColumnIds; - columns = columnTableSchema.Columns; - } else if (isExternalTable(type)) { - columns = currentObjectData?.PathDescription?.ExternalTableDescription?.Columns; - } else { - keyColumnIds = currentObjectData?.PathDescription?.Table?.KeyColumnIds; - columns = currentObjectData?.PathDescription?.Table?.Columns; - } - React.useEffect(() => { const isTable = isTableType(type); @@ -218,7 +175,7 @@ export function ObjectSummary({ component = ; } - return
{component}
; + return component; }; const renderLoader = () => { @@ -236,12 +193,8 @@ export function ObjectSummary({ return ; } case TENANT_SUMMARY_TABS_IDS.schema: { - return loadingSchema ? ( - renderLoader() - ) : ( -
- -
+ return ( + ); } default: { @@ -364,7 +317,7 @@ export function ObjectSummary({ {renderTabs()} - {renderTabContent()} +
{renderTabContent()}
diff --git a/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.scss b/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.scss index b782e5ccbb..1e07dadc0c 100644 --- a/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.scss +++ b/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.scss @@ -1,5 +1,4 @@ .schema-viewer { - padding: 0px 12px; &__key-icon { display: flex; align-items: center; diff --git a/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.tsx b/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.tsx index c1a3018d61..98d3c59414 100644 --- a/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.tsx +++ b/src/containers/Tenant/Schema/SchemaViewer/SchemaViewer.tsx @@ -1,106 +1,58 @@ -import React from 'react'; - -import type {Column} from '@gravity-ui/react-data-table'; import DataTable from '@gravity-ui/react-data-table'; -import {Icon} from '../../../../components/Icon'; -import type {EPathType, TColumnDescription} from '../../../../types/api/schema'; +import {TableSkeleton} from '../../../../components/TableSkeleton/TableSkeleton'; +import type {EPathType} from '../../../../types/api/schema'; import {cn} from '../../../../utils/cn'; import {DEFAULT_TABLE_SETTINGS} from '../../../../utils/constants'; -import {isExternalTable} from '../../utils/schema'; +import {useTypedSelector} from '../../../../utils/hooks'; + +import { + SchemaViewerColumns, + prepareColumnDescriptions, + prepareFamilies, + prepareSchemaTableColumns, +} from './helpers'; import './SchemaViewer.scss'; const b = cn('schema-viewer'); -const SchemaViewerColumns = { - id: 'Id', - name: 'Name', - key: 'Key', - type: 'Type', - notNull: 'NotNull', -}; - interface SchemaViewerProps { - keyColumnIds?: number[]; - columns?: TColumnDescription[]; + className?: string; type?: EPathType; + path?: string; + withFamilies?: boolean; } -export const SchemaViewer = ({keyColumnIds = [], columns = [], type}: SchemaViewerProps) => { - // Keys should be displayd by their order in keyColumnIds (Primary Key) - const keyColumnsOrderValues = React.useMemo(() => { - return keyColumnIds.reduce>((result, keyColumnId, index) => { - // Put columns with negative values, so they will be the first with ascending sort - // Minus keyColumnIds.length for the first key, -1 for the last - result[keyColumnId] = index - keyColumnIds.length; - return result; - }, {}); - }, [keyColumnIds]); +export const SchemaViewer = ({className, type, path, withFamilies = false}: SchemaViewerProps) => { + const {data, loading} = useTypedSelector((state) => state.schema); + const currentObjectData = path ? data[path] : undefined; - let dataTableColumns: Column[] = [ - { - name: SchemaViewerColumns.id, - width: 40, - }, - { - name: SchemaViewerColumns.key, - width: 40, - // Table should start with key columns on sort click - defaultOrder: DataTable.ASCENDING, - sortAccessor: (row) => { - // Values in keyColumnsOrderValues are always negative, so it will be 1 for not key columns - return (row.Id && keyColumnsOrderValues[row.Id]) || 1; - }, - render: ({row}) => { - return row.Id && keyColumnIds.includes(row.Id) ? ( -
- -
- ) : null; - }, - }, - { - name: SchemaViewerColumns.name, - width: 100, - }, - { - name: SchemaViewerColumns.type, - width: 100, - }, - { - name: SchemaViewerColumns.notNull, - width: 100, - // Table should start with notNull columns on sort click - defaultOrder: DataTable.DESCENDING, - render: ({row}) => { - if (row.NotNull) { - return '\u2713'; - } - - return undefined; - }, - }, - ]; - - if (isExternalTable(type)) { - // External tables don't have key columns - dataTableColumns = dataTableColumns.filter( - (column) => column.name !== SchemaViewerColumns.key, - ); - } + const {columns, keyColumnIds} = prepareColumnDescriptions(type, currentObjectData); + const families = prepareFamilies(currentObjectData); return ( -
- +
+ {loading ? ( + + ) : ( + + )}
); }; - -export default SchemaViewer; diff --git a/src/containers/Tenant/Schema/SchemaViewer/helpers.tsx b/src/containers/Tenant/Schema/SchemaViewer/helpers.tsx new file mode 100644 index 0000000000..ed8fcf7781 --- /dev/null +++ b/src/containers/Tenant/Schema/SchemaViewer/helpers.tsx @@ -0,0 +1,198 @@ +import type {ClassNameFormatter} from '@bem-react/classname'; +import DataTable from '@gravity-ui/react-data-table'; +import type {Column} from '@gravity-ui/react-data-table'; + +import {Icon} from '../../../../components/Icon'; +import type { + EPathType, + TColumnDescription, + TColumnTableDescription, + TEvDescribeSchemeResult, + TFamilyDescription, +} from '../../../../types/api/schema'; +import {EColumnCodec} from '../../../../types/api/schema'; +import {isColumnEntityType, isExternalTable, isRowTable, isTableType} from '../../utils/schema'; + +export const SchemaViewerColumns = { + id: 'Id', + name: 'Name', + key: 'Key', + type: 'Type', + notNull: 'NotNull', + familyName: 'Family', + preferredPoolKind: 'Media', + columnCodec: 'Compression', +}; + +function prepareOlapTableSchema(tableSchema: TColumnTableDescription = {}) { + const {Name, Schema} = tableSchema; + + if (Schema) { + const {Columns, KeyColumnNames} = Schema; + const KeyColumnIds = KeyColumnNames?.map((name: string) => { + const column = Columns?.find((el) => el.Name === name); + return column?.Id; + }).filter((id): id is number => id !== undefined); + + return { + Columns, + KeyColumnNames, + Name, + KeyColumnIds, + }; + } + + return { + Name, + }; +} + +function formatColumnCodec(codec?: EColumnCodec) { + if (!codec) { + return null; + } + if (codec === EColumnCodec.ColumnCodecPlain) { + return 'None'; + } + return codec.replace('ColumnCodec', '').toLocaleLowerCase(); +} + +export function prepareColumnDescriptions( + type?: EPathType, + scheme?: TEvDescribeSchemeResult, +): {columns: TColumnDescription[]; keyColumnIds: number[]} { + let keyColumnIds: number[] = []; + let columns: TColumnDescription[] = []; + + if (isTableType(type) && isColumnEntityType(type)) { + const description = scheme?.PathDescription?.ColumnTableDescription; + const columnTableSchema = prepareOlapTableSchema(description); + keyColumnIds = columnTableSchema.KeyColumnIds ?? []; + columns = columnTableSchema.Columns ?? []; + } else if (isExternalTable(type)) { + columns = scheme?.PathDescription?.ExternalTableDescription?.Columns ?? []; + } else { + keyColumnIds = scheme?.PathDescription?.Table?.KeyColumnIds ?? []; + columns = scheme?.PathDescription?.Table?.Columns ?? []; + } + + return {columns, keyColumnIds}; +} + +export function prepareFamilies( + scheme?: TEvDescribeSchemeResult, +): Record { + return ( + scheme?.PathDescription?.Table?.PartitionConfig?.ColumnFamilies?.reduce< + Record + >((acc, family) => { + if (family.Id) { + acc[family.Id] = family; + } + return acc; + }, {}) ?? {} + ); +} + +export function prepareSchemaTableColumns(options: { + type?: EPathType; + b: ClassNameFormatter; + families: Record; + keyColumnIds: number[]; + withFamilies: boolean; +}): Column[] { + const keyColumnsOrderValues = options.keyColumnIds.reduce>( + (result, keyColumnId, index) => { + // Put columns with negative values, so they will be the first with ascending sort + // Minus keyColumnIds.length for the first key, -1 for the last + result[keyColumnId] = index - options.keyColumnIds.length; + return result; + }, + {}, + ); + + const columns: Column[] = [ + { + name: SchemaViewerColumns.id, + width: 40, + }, + ]; + + if (!isExternalTable(options.type)) { + // External tables don't have key columns + columns.push({ + name: SchemaViewerColumns.key, + width: 40, + // Table should start with key columns on sort click + defaultOrder: DataTable.ASCENDING, + // Values in keyColumnsOrderValues are always negative, so it will be 1 for not key columns + sortAccessor: (row) => (row.Id && keyColumnsOrderValues[row.Id]) || 1, + render: ({row}) => { + return row.Id && options.keyColumnIds.includes(row.Id) ? ( +
+ +
+ ) : null; + }, + }); + } + + columns.push( + { + name: SchemaViewerColumns.name, + width: 100, + }, + { + name: SchemaViewerColumns.type, + width: 100, + }, + { + name: SchemaViewerColumns.notNull, + width: 100, + // Table should start with notNull columns on sort click + defaultOrder: DataTable.DESCENDING, + render: ({row}) => { + if (row.NotNull) { + return '\u2713'; + } + + return undefined; + }, + }, + ); + + if (options.withFamilies && isRowTable(options.type)) { + columns.push( + { + name: SchemaViewerColumns.familyName, + width: 100, + render: ({row}) => (row.Family ? options.families[row.Family].Name : undefined), + sortAccessor: (row) => (row.Family ? options.families[row.Family].Name : undefined), + }, + { + name: SchemaViewerColumns.preferredPoolKind, + width: 100, + render: ({row}) => + row.Family + ? options.families[row.Family].StorageConfig?.Data?.PreferredPoolKind + : undefined, + sortAccessor: (row) => + row.Family + ? options.families[row.Family].StorageConfig?.Data?.PreferredPoolKind + : undefined, + }, + { + name: SchemaViewerColumns.columnCodec, + width: 100, + render: ({row}) => + row.Family + ? formatColumnCodec(options.families[row.Family].ColumnCodec) + : undefined, + sortAccessor: (row) => + row.Family ? options.families[row.Family].ColumnCodec : undefined, + }, + ); + } + + return columns; +} diff --git a/src/containers/Tenant/utils/schema.ts b/src/containers/Tenant/utils/schema.ts index fd77fa5e98..e24d2dc0ea 100644 --- a/src/containers/Tenant/utils/schema.ts +++ b/src/containers/Tenant/utils/schema.ts @@ -245,3 +245,4 @@ export const isPathTypeWithTopic = (type?: EPathType) => // ==================== export const isExternalTable = (type?: EPathType) => type === EPathType.EPathTypeExternalTable; +export const isRowTable = (type?: EPathType) => type === EPathType.EPathTypeTable; diff --git a/src/store/reducers/tenant/constants.ts b/src/store/reducers/tenant/constants.ts index 3d6a297b04..d7818309a2 100644 --- a/src/store/reducers/tenant/constants.ts +++ b/src/store/reducers/tenant/constants.ts @@ -13,6 +13,7 @@ export const TENANT_QUERY_TABS_ID = { export const TENANT_DIAGNOSTICS_TABS_IDS = { overview: 'overview', + schema: 'schema', topQueries: 'topQueries', topShards: 'topShards', nodes: 'nodes', diff --git a/src/types/api/schema/table.ts b/src/types/api/schema/table.ts index 44b30ca663..86ec247b47 100644 --- a/src/types/api/schema/table.ts +++ b/src/types/api/schema/table.ts @@ -334,7 +334,7 @@ interface TPipelineConfig { EnableSoftUpdates?: boolean; } -interface TFamilyDescription { +export interface TFamilyDescription { Id?: number; Room?: number; /** @deprecated use ColumnCodec */