From cff521656cce2b31f1557363c2f26369d6e13df8 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 17 Nov 2025 18:35:23 +0300 Subject: [PATCH 1/7] fix: progress is not shown for export --- src/containers/Operations/columns.tsx | 61 ++++++++------ src/containers/Operations/i18n/en.json | 8 ++ src/containers/Operations/utils.ts | 111 +++++++++++++++++++++++++ src/types/api/operations.ts | 91 +++++++++++++++++++- 4 files changed, 243 insertions(+), 28 deletions(-) create mode 100644 src/containers/Operations/utils.ts diff --git a/src/containers/Operations/columns.tsx b/src/containers/Operations/columns.tsx index 9e0b7e656b..b302a65c2b 100644 --- a/src/containers/Operations/columns.tsx +++ b/src/containers/Operations/columns.tsx @@ -15,6 +15,7 @@ import {parseProtobufTimestampToMs} from '../../utils/timeParsers'; import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants'; import i18n from './i18n'; +import {getOperationProgress} from './utils'; import './Operations.scss'; @@ -28,13 +29,15 @@ export function getColumns({ kind: OperationKind; }): DataTableColumn[] { const isBuildIndex = kind === 'buildindex'; + const isImportOrExport = kind === 'import/s3' || kind === 'export/s3' || kind === 'export/yt'; // Helper function to get description tooltip content const getDescriptionTooltip = (operation: TOperation): string => { - if (!operation.metadata?.description) { + const metadata = operation.metadata as IndexBuildMetadata | undefined; + if (!metadata?.description) { return ''; } - return JSON.stringify(operation.metadata.description, null, 2); + return JSON.stringify(metadata.description, null, 2); }; const columns: DataTableColumn[] = [ @@ -72,34 +75,38 @@ export function getColumns({ }, ]; - // Add buildindex-specific columns + // Add buildindex-specific state column if (isBuildIndex) { - columns.push( - { - name: COLUMNS_NAMES.STATE, - header: COLUMNS_TITLES[COLUMNS_NAMES.STATE], - render: ({row}) => { - const metadata = row.metadata as IndexBuildMetadata | undefined; - if (!metadata?.state) { - return EMPTY_DATA_PLACEHOLDER; - } - return metadata.state; - }, + columns.push({ + name: COLUMNS_NAMES.STATE, + header: COLUMNS_TITLES[COLUMNS_NAMES.STATE], + render: ({row}) => { + const metadata = row.metadata as IndexBuildMetadata | undefined; + if (!metadata?.state) { + return EMPTY_DATA_PLACEHOLDER; + } + return metadata.state; }, - { - name: COLUMNS_NAMES.PROGRESS, - header: COLUMNS_TITLES[COLUMNS_NAMES.PROGRESS], - render: ({row}) => { - const metadata = row.metadata as IndexBuildMetadata | undefined; - if (metadata?.progress === undefined) { - return EMPTY_DATA_PLACEHOLDER; - } - return `${Math.round(metadata.progress)}%`; - }, + }); + } + + // Add progress column for operations that have progress data + if (isBuildIndex || isImportOrExport) { + columns.push({ + name: COLUMNS_NAMES.PROGRESS, + header: COLUMNS_TITLES[COLUMNS_NAMES.PROGRESS], + render: ({row}) => { + const progress = getOperationProgress(row, (key) => i18n(key as keyof typeof i18n)); + if (progress === null) { + return EMPTY_DATA_PLACEHOLDER; + } + return progress; }, - ); - } else { - // Add standard columns for non-buildindex operations + }); + } + + // Add standard columns for non-buildindex operations + if (!isBuildIndex) { columns.push( { name: COLUMNS_NAMES.CREATED_BY, diff --git a/src/containers/Operations/i18n/en.json b/src/containers/Operations/i18n/en.json index 791d01f9dd..6773d51909 100644 --- a/src/containers/Operations/i18n/en.json +++ b/src/containers/Operations/i18n/en.json @@ -16,6 +16,14 @@ "column_state": "State", "column_progress": "Progress", "label_duration-ongoing": "{{value}} (ongoing)", + "progress_unspecified": "Unspecified", + "progress_preparing": "Preparing", + "progress_transfer_data": "Transferring Data", + "progress_build_indexes": "Building Indexes", + "progress_done": "Done", + "progress_cancellation": "Cancelling", + "progress_cancelled": "Cancelled", + "progress_create_changefeeds": "Creating Changefeeds", "header_cancel": "Cancel operation", "header_forget": "Forget operation", "text_cancel": "The operation will be cancelled. Do you want to proceed?", diff --git a/src/containers/Operations/utils.ts b/src/containers/Operations/utils.ts new file mode 100644 index 0000000000..3ddeed13f8 --- /dev/null +++ b/src/containers/Operations/utils.ts @@ -0,0 +1,111 @@ +import type { + ExportToS3Metadata, + ExportToYtMetadata, + ImportFromS3Metadata, + IndexBuildMetadata, + TOperation, +} from '../../types/api/operations'; + +/** + * Calculate progress percentage from Import/Export metadata + * + * Calculates overall progress based on items_progress array: + * - Sums all parts_total and parts_completed across all items + * - Returns percentage rounded to nearest integer + * + * @param metadata - Import/Export operation metadata + * @returns Progress percentage (0-100) or null if cannot be calculated + */ +export function calculateImportExportProgress( + metadata: ImportFromS3Metadata | ExportToS3Metadata | ExportToYtMetadata | undefined, +): number | null { + if (!metadata?.items_progress || metadata.items_progress.length === 0) { + return null; + } + + let totalParts = 0; + let completedParts = 0; + + for (const item of metadata.items_progress) { + if (item.parts_total !== undefined && item.parts_total > 0) { + totalParts += item.parts_total; + completedParts += item.parts_completed || 0; + } + } + + if (totalParts === 0) { + return null; + } + + return Math.round((completedParts / totalParts) * 100); +} + +/** + * Get progress display value for an operation + * + * Handles different progress formats: + * - BuildIndex: numeric progress (0-100) -> "75%" + * - Import/Export: calculated from items_progress -> "45%" or enum value -> "Done" + * + * @param operation - Operation to get progress for + * @param translateProgress - Function to translate progress enum values (i18n) + * @returns Formatted progress string or null if no progress available + */ +export function getOperationProgress( + operation: TOperation, + translateProgress: (key: string) => string, +): string | null { + const metadata = operation.metadata; + + if (!metadata) { + return null; + } + + // BuildIndex: numeric progress (0-100) + if ('progress' in metadata && typeof metadata.progress === 'number') { + const buildIndexMetadata = metadata as IndexBuildMetadata; + if (buildIndexMetadata.progress !== undefined) { + return `${Math.round(buildIndexMetadata.progress)}%`; + } + } + + // Import/Export: calculate from items_progress or show enum value + const importExportMetadata = metadata as + | ImportFromS3Metadata + | ExportToS3Metadata + | ExportToYtMetadata + | undefined; + + if (importExportMetadata) { + // Try to calculate percentage from items_progress + const calculatedProgress = calculateImportExportProgress(importExportMetadata); + if (calculatedProgress !== null) { + return `${calculatedProgress}%`; + } + + // Fallback to enum progress value + if (importExportMetadata.progress) { + const progressValue = + typeof importExportMetadata.progress === 'string' + ? importExportMetadata.progress + : String(importExportMetadata.progress); + + // Convert PROGRESS_DONE -> progress_done for i18n key + const i18nKey = progressValue.toLowerCase(); + + // Try to get translated value, fallback to original value + try { + const translated = translateProgress(i18nKey); + if (translated && translated !== i18nKey) { + return translated; + } + } catch { + // Translation function failed, use original value + } + + return progressValue; + } + } + + return null; +} diff --git a/src/types/api/operations.ts b/src/types/api/operations.ts index 94b7e9fb0c..54e69f9e56 100644 --- a/src/types/api/operations.ts +++ b/src/types/api/operations.ts @@ -88,7 +88,96 @@ export enum IndexBuildState { STATE_REJECTED = 'STATE_REJECTED', } -export type TOperationMetadata = IndexBuildMetadata; +/** + * Import/Export progress enum + * + * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto + */ +export enum ImportExportProgress { + PROGRESS_UNSPECIFIED = 'PROGRESS_UNSPECIFIED', + PROGRESS_PREPARING = 'PROGRESS_PREPARING', + PROGRESS_TRANSFER_DATA = 'PROGRESS_TRANSFER_DATA', + PROGRESS_BUILD_INDEXES = 'PROGRESS_BUILD_INDEXES', + PROGRESS_DONE = 'PROGRESS_DONE', + PROGRESS_CANCELLATION = 'PROGRESS_CANCELLATION', + PROGRESS_CANCELLED = 'PROGRESS_CANCELLED', + PROGRESS_CREATE_CHANGEFEEDS = 'PROGRESS_CREATE_CHANGEFEEDS', +} + +/** + * Import/Export item progress + * + * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto + */ +export interface ImportExportItemProgress { + parts_total?: number; + parts_completed?: number; + start_time?: IProtobufTimeObject; + end_time?: IProtobufTimeObject; +} + +/** + * Import from S3 metadata + * + * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto#L108 + */ +export interface ImportFromS3Metadata { + '@type'?: 'type.googleapis.com/Ydb.Import.ImportFromS3Metadata'; + settings?: { + endpoint?: string; + scheme?: string; + bucket?: string; + items?: Array<{ + source_prefix?: string; + source_path?: string; + destination_path?: string; + }>; + [key: string]: unknown; + }; + progress?: ImportExportProgress | string; + items_progress?: ImportExportItemProgress[]; +} + +/** + * Export to S3 metadata + * + * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_export.proto + */ +export interface ExportToS3Metadata { + '@type'?: 'type.googleapis.com/Ydb.Export.ExportToS3Metadata'; + settings?: { + endpoint?: string; + scheme?: string; + bucket?: string; + items?: Array<{ + source_path?: string; + destination_prefix?: string; + }>; + [key: string]: unknown; + }; + progress?: ImportExportProgress | string; + items_progress?: ImportExportItemProgress[]; +} + +/** + * Export to YT metadata + * + * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_export.proto + */ +export interface ExportToYtMetadata { + '@type'?: 'type.googleapis.com/Ydb.Export.ExportToYtMetadata'; + settings?: { + [key: string]: unknown; + }; + progress?: ImportExportProgress | string; + items_progress?: ImportExportItemProgress[]; +} + +export type TOperationMetadata = + | IndexBuildMetadata + | ImportFromS3Metadata + | ExportToS3Metadata + | ExportToYtMetadata; export interface TCostInfo { consumed_units?: number; From 53e6f399f60ee222e95fb6b7ef8981d75adf1355 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 17 Nov 2025 18:53:32 +0300 Subject: [PATCH 2/7] fix: review fixes --- src/containers/Operations/columns.tsx | 12 +++++++----- src/containers/Operations/utils.ts | 6 +++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/containers/Operations/columns.tsx b/src/containers/Operations/columns.tsx index b302a65c2b..141184e7ee 100644 --- a/src/containers/Operations/columns.tsx +++ b/src/containers/Operations/columns.tsx @@ -29,11 +29,10 @@ export function getColumns({ kind: OperationKind; }): DataTableColumn[] { const isBuildIndex = kind === 'buildindex'; - const isImportOrExport = kind === 'import/s3' || kind === 'export/s3' || kind === 'export/yt'; + const isImportOrExport = ['import/s3', 'export/s3', 'export/yt'].includes(kind); - // Helper function to get description tooltip content - const getDescriptionTooltip = (operation: TOperation): string => { - const metadata = operation.metadata as IndexBuildMetadata | undefined; + // Helper function to get description tooltip content (buildindex-only) + const getDescriptionTooltip = (metadata?: IndexBuildMetadata): string => { if (!metadata?.description) { return ''; } @@ -50,7 +49,10 @@ export function getColumns({ return EMPTY_DATA_PLACEHOLDER; } - const tooltipContent = isBuildIndex ? getDescriptionTooltip(row) || row.id : row.id; + const tooltipContent = isBuildIndex + ? getDescriptionTooltip(row.metadata as IndexBuildMetadata | undefined) || + row.id + : row.id; return ( diff --git a/src/containers/Operations/utils.ts b/src/containers/Operations/utils.ts index 3ddeed13f8..239d948463 100644 --- a/src/containers/Operations/utils.ts +++ b/src/containers/Operations/utils.ts @@ -61,10 +61,10 @@ export function getOperationProgress( return null; } - // BuildIndex: numeric progress (0-100) - if ('progress' in metadata && typeof metadata.progress === 'number') { + // BuildIndex: numeric progress (0-100), discriminated by @type + if (metadata['@type'] === 'type.googleapis.com/Ydb.Table.IndexBuildMetadata') { const buildIndexMetadata = metadata as IndexBuildMetadata; - if (buildIndexMetadata.progress !== undefined) { + if (typeof buildIndexMetadata.progress === 'number') { return `${Math.round(buildIndexMetadata.progress)}%`; } } From d7b47e51d1402810b8cd95aa2e53f4b7dc1cdd15 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 17 Nov 2025 19:56:56 +0300 Subject: [PATCH 3/7] fix: review fixes --- src/containers/Operations/columns.tsx | 2 +- src/containers/Operations/i18n/en.json | 16 ++++++------ src/containers/Operations/utils.ts | 35 +++++++++++++++++++------- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/containers/Operations/columns.tsx b/src/containers/Operations/columns.tsx index 141184e7ee..3beaafe3c4 100644 --- a/src/containers/Operations/columns.tsx +++ b/src/containers/Operations/columns.tsx @@ -98,7 +98,7 @@ export function getColumns({ name: COLUMNS_NAMES.PROGRESS, header: COLUMNS_TITLES[COLUMNS_NAMES.PROGRESS], render: ({row}) => { - const progress = getOperationProgress(row, (key) => i18n(key as keyof typeof i18n)); + const progress = getOperationProgress(row, i18n); if (progress === null) { return EMPTY_DATA_PLACEHOLDER; } diff --git a/src/containers/Operations/i18n/en.json b/src/containers/Operations/i18n/en.json index 6773d51909..8a02cdcbdf 100644 --- a/src/containers/Operations/i18n/en.json +++ b/src/containers/Operations/i18n/en.json @@ -16,14 +16,14 @@ "column_state": "State", "column_progress": "Progress", "label_duration-ongoing": "{{value}} (ongoing)", - "progress_unspecified": "Unspecified", - "progress_preparing": "Preparing", - "progress_transfer_data": "Transferring Data", - "progress_build_indexes": "Building Indexes", - "progress_done": "Done", - "progress_cancellation": "Cancelling", - "progress_cancelled": "Cancelled", - "progress_create_changefeeds": "Creating Changefeeds", + "value_progress_unspecified": "Unspecified", + "value_progress_preparing": "Preparing", + "value_progress_transfer_data": "Transferring Data", + "value_progress_build_indexes": "Building Indexes", + "value_progress_done": "Done", + "value_progress_cancellation": "Cancelling", + "value_progress_cancelled": "Cancelled", + "value_progress_create_changefeeds": "Creating Changefeeds", "header_cancel": "Cancel operation", "header_forget": "Forget operation", "text_cancel": "The operation will be cancelled. Do you want to proceed?", diff --git a/src/containers/Operations/utils.ts b/src/containers/Operations/utils.ts index 239d948463..9fc750f31f 100644 --- a/src/containers/Operations/utils.ts +++ b/src/containers/Operations/utils.ts @@ -6,6 +6,18 @@ import type { TOperation, } from '../../types/api/operations'; +// i18n keys for import/export progress enum values +// value_progress_unspecified, value_progress_preparing, etc. +export type OperationProgressKey = + | 'value_progress_unspecified' + | 'value_progress_preparing' + | 'value_progress_transfer_data' + | 'value_progress_build_indexes' + | 'value_progress_done' + | 'value_progress_cancellation' + | 'value_progress_cancelled' + | 'value_progress_create_changefeeds'; + /** * Calculate progress percentage from Import/Export metadata * @@ -53,7 +65,7 @@ export function calculateImportExportProgress( */ export function getOperationProgress( operation: TOperation, - translateProgress: (key: string) => string, + translateProgress: (key: OperationProgressKey) => string, ): string | null { const metadata = operation.metadata; @@ -70,13 +82,16 @@ export function getOperationProgress( } // Import/Export: calculate from items_progress or show enum value - const importExportMetadata = metadata as - | ImportFromS3Metadata - | ExportToS3Metadata - | ExportToYtMetadata - | undefined; + if ( + metadata['@type'] === 'type.googleapis.com/Ydb.Import.ImportFromS3Metadata' || + metadata['@type'] === 'type.googleapis.com/Ydb.Export.ExportToS3Metadata' || + metadata['@type'] === 'type.googleapis.com/Ydb.Export.ExportToYtMetadata' + ) { + const importExportMetadata = metadata as + | ImportFromS3Metadata + | ExportToS3Metadata + | ExportToYtMetadata; - if (importExportMetadata) { // Try to calculate percentage from items_progress const calculatedProgress = calculateImportExportProgress(importExportMetadata); if (calculatedProgress !== null) { @@ -90,8 +105,10 @@ export function getOperationProgress( ? importExportMetadata.progress : String(importExportMetadata.progress); - // Convert PROGRESS_DONE -> progress_done for i18n key - const i18nKey = progressValue.toLowerCase(); + // Backend enums are PROGRESS_DONE, PROGRESS_PREPARING, etc. + // Map them to i18n keys value_progress_done, value_progress_preparing, etc. + const normalized = progressValue.toLowerCase(); // progress_done + const i18nKey = `value_${normalized}` as OperationProgressKey; // Try to get translated value, fallback to original value try { From 848ffd3d9a0cfe2fbb2f8f91b61cc770e4b7fdb9 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Mon, 17 Nov 2025 22:11:08 +0300 Subject: [PATCH 4/7] fix: nanotuting --- src/containers/Operations/utils.ts | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/containers/Operations/utils.ts b/src/containers/Operations/utils.ts index 9fc750f31f..1c3d836892 100644 --- a/src/containers/Operations/utils.ts +++ b/src/containers/Operations/utils.ts @@ -2,7 +2,6 @@ import type { ExportToS3Metadata, ExportToYtMetadata, ImportFromS3Metadata, - IndexBuildMetadata, TOperation, } from '../../types/api/operations'; @@ -73,9 +72,8 @@ export function getOperationProgress( return null; } - // BuildIndex: numeric progress (0-100), discriminated by @type if (metadata['@type'] === 'type.googleapis.com/Ydb.Table.IndexBuildMetadata') { - const buildIndexMetadata = metadata as IndexBuildMetadata; + const buildIndexMetadata = metadata; if (typeof buildIndexMetadata.progress === 'number') { return `${Math.round(buildIndexMetadata.progress)}%`; } @@ -87,10 +85,7 @@ export function getOperationProgress( metadata['@type'] === 'type.googleapis.com/Ydb.Export.ExportToS3Metadata' || metadata['@type'] === 'type.googleapis.com/Ydb.Export.ExportToYtMetadata' ) { - const importExportMetadata = metadata as - | ImportFromS3Metadata - | ExportToS3Metadata - | ExportToYtMetadata; + const importExportMetadata = metadata; // Try to calculate percentage from items_progress const calculatedProgress = calculateImportExportProgress(importExportMetadata); @@ -105,20 +100,15 @@ export function getOperationProgress( ? importExportMetadata.progress : String(importExportMetadata.progress); - // Backend enums are PROGRESS_DONE, PROGRESS_PREPARING, etc. - // Map them to i18n keys value_progress_done, value_progress_preparing, etc. const normalized = progressValue.toLowerCase(); // progress_done const i18nKey = `value_${normalized}` as OperationProgressKey; - // Try to get translated value, fallback to original value try { const translated = translateProgress(i18nKey); if (translated && translated !== i18nKey) { return translated; } - } catch { - // Translation function failed, use original value - } + } catch {} return progressValue; } From 6f7a59b9408470a68f7ffb3920f15ef68f4487d1 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 18 Nov 2025 11:52:42 +0300 Subject: [PATCH 5/7] fix: use constants --- src/containers/Operations/columns.tsx | 18 ++++---- src/containers/Operations/utils.ts | 61 ++++++++++++++++++++++++--- src/types/api/operations.ts | 21 +++++---- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/containers/Operations/columns.tsx b/src/containers/Operations/columns.tsx index 3beaafe3c4..625392fc53 100644 --- a/src/containers/Operations/columns.tsx +++ b/src/containers/Operations/columns.tsx @@ -15,10 +15,12 @@ import {parseProtobufTimestampToMs} from '../../utils/timeParsers'; import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants'; import i18n from './i18n'; -import {getOperationProgress} from './utils'; +import {getOperationProgress, isIndexBuildMetadata} from './utils'; import './Operations.scss'; +const IMPORT_EXPORT_KINDS: OperationKind[] = ['import/s3', 'export/s3', 'export/yt']; + export function getColumns({ database, refreshTable, @@ -29,13 +31,16 @@ export function getColumns({ kind: OperationKind; }): DataTableColumn[] { const isBuildIndex = kind === 'buildindex'; - const isImportOrExport = ['import/s3', 'export/s3', 'export/yt'].includes(kind); + const isImportOrExport = IMPORT_EXPORT_KINDS.includes(kind); // Helper function to get description tooltip content (buildindex-only) - const getDescriptionTooltip = (metadata?: IndexBuildMetadata): string => { - if (!metadata?.description) { + const getDescriptionTooltip = (operation: TOperation): string => { + const {metadata} = operation; + + if (!isIndexBuildMetadata(metadata) || !metadata.description) { return ''; } + return JSON.stringify(metadata.description, null, 2); }; @@ -49,10 +54,7 @@ export function getColumns({ return EMPTY_DATA_PLACEHOLDER; } - const tooltipContent = isBuildIndex - ? getDescriptionTooltip(row.metadata as IndexBuildMetadata | undefined) || - row.id - : row.id; + const tooltipContent = isBuildIndex ? getDescriptionTooltip(row) || row.id : row.id; return ( diff --git a/src/containers/Operations/utils.ts b/src/containers/Operations/utils.ts index 1c3d836892..def0ef125b 100644 --- a/src/containers/Operations/utils.ts +++ b/src/containers/Operations/utils.ts @@ -2,8 +2,61 @@ import type { ExportToS3Metadata, ExportToYtMetadata, ImportFromS3Metadata, + IndexBuildMetadata, TOperation, } from '../../types/api/operations'; +import {OPERATION_METADATA_TYPE_URLS} from '../../types/api/operations'; + +// Type guards for operation metadata kinds +export function isIndexBuildMetadata( + metadata: TOperation['metadata'], +): metadata is IndexBuildMetadata { + if (!metadata) { + return false; + } + + return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.IndexBuild; +} + +export function isImportFromS3Metadata( + metadata: TOperation['metadata'], +): metadata is ImportFromS3Metadata { + if (!metadata) { + return false; + } + + return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.ImportFromS3; +} + +export function isExportToS3Metadata( + metadata: TOperation['metadata'], +): metadata is ExportToS3Metadata { + if (!metadata) { + return false; + } + + return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.ExportToS3; +} + +export function isExportToYtMetadata( + metadata: TOperation['metadata'], +): metadata is ExportToYtMetadata { + if (!metadata) { + return false; + } + + return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.ExportToYt; +} + +export function isImportExportMetadata( + metadata: TOperation['metadata'], +): metadata is ImportFromS3Metadata | ExportToS3Metadata | ExportToYtMetadata { + return ( + isImportFromS3Metadata(metadata) || + isExportToS3Metadata(metadata) || + isExportToYtMetadata(metadata) + ); +} // i18n keys for import/export progress enum values // value_progress_unspecified, value_progress_preparing, etc. @@ -72,7 +125,7 @@ export function getOperationProgress( return null; } - if (metadata['@type'] === 'type.googleapis.com/Ydb.Table.IndexBuildMetadata') { + if (isIndexBuildMetadata(metadata)) { const buildIndexMetadata = metadata; if (typeof buildIndexMetadata.progress === 'number') { return `${Math.round(buildIndexMetadata.progress)}%`; @@ -80,11 +133,7 @@ export function getOperationProgress( } // Import/Export: calculate from items_progress or show enum value - if ( - metadata['@type'] === 'type.googleapis.com/Ydb.Import.ImportFromS3Metadata' || - metadata['@type'] === 'type.googleapis.com/Ydb.Export.ExportToS3Metadata' || - metadata['@type'] === 'type.googleapis.com/Ydb.Export.ExportToYtMetadata' - ) { + if (isImportExportMetadata(metadata)) { const importExportMetadata = metadata; // Try to calculate percentage from items_progress diff --git a/src/types/api/operations.ts b/src/types/api/operations.ts index 54e69f9e56..fcce861646 100644 --- a/src/types/api/operations.ts +++ b/src/types/api/operations.ts @@ -88,9 +88,18 @@ export enum IndexBuildState { STATE_REJECTED = 'STATE_REJECTED', } +export const OPERATION_METADATA_TYPE_URLS = { + IndexBuild: 'type.googleapis.com/Ydb.Table.IndexBuildMetadata', + ImportFromS3: 'type.googleapis.com/Ydb.Import.ImportFromS3Metadata', + ExportToS3: 'type.googleapis.com/Ydb.Export.ExportToS3Metadata', + ExportToYt: 'type.googleapis.com/Ydb.Export.ExportToYtMetadata', +} as const; + +export type OperationMetadataTypeUrl = + (typeof OPERATION_METADATA_TYPE_URLS)[keyof typeof OPERATION_METADATA_TYPE_URLS]; + /** * Import/Export progress enum - * * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto */ export enum ImportExportProgress { @@ -106,7 +115,6 @@ export enum ImportExportProgress { /** * Import/Export item progress - * * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto */ export interface ImportExportItemProgress { @@ -118,11 +126,10 @@ export interface ImportExportItemProgress { /** * Import from S3 metadata - * * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto#L108 */ export interface ImportFromS3Metadata { - '@type'?: 'type.googleapis.com/Ydb.Import.ImportFromS3Metadata'; + '@type'?: typeof OPERATION_METADATA_TYPE_URLS.ImportFromS3; settings?: { endpoint?: string; scheme?: string; @@ -140,11 +147,10 @@ export interface ImportFromS3Metadata { /** * Export to S3 metadata - * * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_export.proto */ export interface ExportToS3Metadata { - '@type'?: 'type.googleapis.com/Ydb.Export.ExportToS3Metadata'; + '@type'?: typeof OPERATION_METADATA_TYPE_URLS.ExportToS3; settings?: { endpoint?: string; scheme?: string; @@ -161,11 +167,10 @@ export interface ExportToS3Metadata { /** * Export to YT metadata - * * source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_export.proto */ export interface ExportToYtMetadata { - '@type'?: 'type.googleapis.com/Ydb.Export.ExportToYtMetadata'; + '@type'?: typeof OPERATION_METADATA_TYPE_URLS.ExportToYt; settings?: { [key: string]: unknown; }; From f02a6de89988cf7c9e572615df725a0eb46f2b56 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 18 Nov 2025 12:22:53 +0300 Subject: [PATCH 6/7] fix: i18n --- src/containers/Operations/utils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/containers/Operations/utils.ts b/src/containers/Operations/utils.ts index def0ef125b..ae68c7dc9f 100644 --- a/src/containers/Operations/utils.ts +++ b/src/containers/Operations/utils.ts @@ -149,8 +149,11 @@ export function getOperationProgress( ? importExportMetadata.progress : String(importExportMetadata.progress); - const normalized = progressValue.toLowerCase(); // progress_done - const i18nKey = `value_${normalized}` as OperationProgressKey; + // Backend enums are usually PROGRESS_DONE, PROGRESS_PREPARING, etc. + // Normalize by stripping optional PROGRESS_ prefix and lowercasing. + // Both "PROGRESS_DONE" and "DONE" will map to "value_progress_done". + const base = progressValue.replace(/^PROGRESS_/, '').toLowerCase(); // done + const i18nKey = `value_progress_${base}` as OperationProgressKey; try { const translated = translateProgress(i18nKey); From 6c05177de14a0b392b3dc0ca0e68592132ae8c71 Mon Sep 17 00:00:00 2001 From: Anton Standrik Date: Tue, 18 Nov 2025 12:26:21 +0300 Subject: [PATCH 7/7] fix: nanofix --- src/containers/Operations/utils.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/containers/Operations/utils.ts b/src/containers/Operations/utils.ts index ae68c7dc9f..2bb436d701 100644 --- a/src/containers/Operations/utils.ts +++ b/src/containers/Operations/utils.ts @@ -126,28 +126,25 @@ export function getOperationProgress( } if (isIndexBuildMetadata(metadata)) { - const buildIndexMetadata = metadata; - if (typeof buildIndexMetadata.progress === 'number') { - return `${Math.round(buildIndexMetadata.progress)}%`; + if (typeof metadata.progress === 'number') { + return `${Math.round(metadata.progress)}%`; } } // Import/Export: calculate from items_progress or show enum value if (isImportExportMetadata(metadata)) { - const importExportMetadata = metadata; - // Try to calculate percentage from items_progress - const calculatedProgress = calculateImportExportProgress(importExportMetadata); + const calculatedProgress = calculateImportExportProgress(metadata); if (calculatedProgress !== null) { return `${calculatedProgress}%`; } // Fallback to enum progress value - if (importExportMetadata.progress) { + if (metadata.progress) { const progressValue = - typeof importExportMetadata.progress === 'string' - ? importExportMetadata.progress - : String(importExportMetadata.progress); + typeof metadata.progress === 'string' + ? metadata.progress + : String(metadata.progress); // Backend enums are usually PROGRESS_DONE, PROGRESS_PREPARING, etc. // Normalize by stripping optional PROGRESS_ prefix and lowercasing.