Skip to content

Commit 11c971a

Browse files
authored
fix: progress is not shown for export (#3092)
1 parent eb54851 commit 11c971a

File tree

4 files changed

+309
-29
lines changed

4 files changed

+309
-29
lines changed

src/containers/Operations/columns.tsx

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ import {parseProtobufTimestampToMs} from '../../utils/timeParsers';
1515

1616
import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants';
1717
import i18n from './i18n';
18+
import {getOperationProgress, isIndexBuildMetadata} from './utils';
1819

1920
import './Operations.scss';
2021

22+
const IMPORT_EXPORT_KINDS: OperationKind[] = ['import/s3', 'export/s3', 'export/yt'];
23+
2124
export function getColumns({
2225
database,
2326
refreshTable,
@@ -28,13 +31,17 @@ export function getColumns({
2831
kind: OperationKind;
2932
}): DataTableColumn<TOperation>[] {
3033
const isBuildIndex = kind === 'buildindex';
34+
const isImportOrExport = IMPORT_EXPORT_KINDS.includes(kind);
3135

32-
// Helper function to get description tooltip content
36+
// Helper function to get description tooltip content (buildindex-only)
3337
const getDescriptionTooltip = (operation: TOperation): string => {
34-
if (!operation.metadata?.description) {
38+
const {metadata} = operation;
39+
40+
if (!isIndexBuildMetadata(metadata) || !metadata.description) {
3541
return '';
3642
}
37-
return JSON.stringify(operation.metadata.description, null, 2);
43+
44+
return JSON.stringify(metadata.description, null, 2);
3845
};
3946

4047
const columns: DataTableColumn<TOperation>[] = [
@@ -72,34 +79,38 @@ export function getColumns({
7279
},
7380
];
7481

75-
// Add buildindex-specific columns
82+
// Add buildindex-specific state column
7683
if (isBuildIndex) {
77-
columns.push(
78-
{
79-
name: COLUMNS_NAMES.STATE,
80-
header: COLUMNS_TITLES[COLUMNS_NAMES.STATE],
81-
render: ({row}) => {
82-
const metadata = row.metadata as IndexBuildMetadata | undefined;
83-
if (!metadata?.state) {
84-
return EMPTY_DATA_PLACEHOLDER;
85-
}
86-
return metadata.state;
87-
},
84+
columns.push({
85+
name: COLUMNS_NAMES.STATE,
86+
header: COLUMNS_TITLES[COLUMNS_NAMES.STATE],
87+
render: ({row}) => {
88+
const metadata = row.metadata as IndexBuildMetadata | undefined;
89+
if (!metadata?.state) {
90+
return EMPTY_DATA_PLACEHOLDER;
91+
}
92+
return metadata.state;
8893
},
89-
{
90-
name: COLUMNS_NAMES.PROGRESS,
91-
header: COLUMNS_TITLES[COLUMNS_NAMES.PROGRESS],
92-
render: ({row}) => {
93-
const metadata = row.metadata as IndexBuildMetadata | undefined;
94-
if (metadata?.progress === undefined) {
95-
return EMPTY_DATA_PLACEHOLDER;
96-
}
97-
return `${Math.round(metadata.progress)}%`;
98-
},
94+
});
95+
}
96+
97+
// Add progress column for operations that have progress data
98+
if (isBuildIndex || isImportOrExport) {
99+
columns.push({
100+
name: COLUMNS_NAMES.PROGRESS,
101+
header: COLUMNS_TITLES[COLUMNS_NAMES.PROGRESS],
102+
render: ({row}) => {
103+
const progress = getOperationProgress(row, i18n);
104+
if (progress === null) {
105+
return EMPTY_DATA_PLACEHOLDER;
106+
}
107+
return progress;
99108
},
100-
);
101-
} else {
102-
// Add standard columns for non-buildindex operations
109+
});
110+
}
111+
112+
// Add standard columns for non-buildindex operations
113+
if (!isBuildIndex) {
103114
columns.push(
104115
{
105116
name: COLUMNS_NAMES.CREATED_BY,

src/containers/Operations/i18n/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@
1616
"column_state": "State",
1717
"column_progress": "Progress",
1818
"label_duration-ongoing": "{{value}} (ongoing)",
19+
"value_progress_unspecified": "Unspecified",
20+
"value_progress_preparing": "Preparing",
21+
"value_progress_transfer_data": "Transferring Data",
22+
"value_progress_build_indexes": "Building Indexes",
23+
"value_progress_done": "Done",
24+
"value_progress_cancellation": "Cancelling",
25+
"value_progress_cancelled": "Cancelled",
26+
"value_progress_create_changefeeds": "Creating Changefeeds",
1927
"header_cancel": "Cancel operation",
2028
"header_forget": "Forget operation",
2129
"text_cancel": "The operation will be cancelled. Do you want to proceed?",

src/containers/Operations/utils.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import type {
2+
ExportToS3Metadata,
3+
ExportToYtMetadata,
4+
ImportFromS3Metadata,
5+
IndexBuildMetadata,
6+
TOperation,
7+
} from '../../types/api/operations';
8+
import {OPERATION_METADATA_TYPE_URLS} from '../../types/api/operations';
9+
10+
// Type guards for operation metadata kinds
11+
export function isIndexBuildMetadata(
12+
metadata: TOperation['metadata'],
13+
): metadata is IndexBuildMetadata {
14+
if (!metadata) {
15+
return false;
16+
}
17+
18+
return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.IndexBuild;
19+
}
20+
21+
export function isImportFromS3Metadata(
22+
metadata: TOperation['metadata'],
23+
): metadata is ImportFromS3Metadata {
24+
if (!metadata) {
25+
return false;
26+
}
27+
28+
return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.ImportFromS3;
29+
}
30+
31+
export function isExportToS3Metadata(
32+
metadata: TOperation['metadata'],
33+
): metadata is ExportToS3Metadata {
34+
if (!metadata) {
35+
return false;
36+
}
37+
38+
return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.ExportToS3;
39+
}
40+
41+
export function isExportToYtMetadata(
42+
metadata: TOperation['metadata'],
43+
): metadata is ExportToYtMetadata {
44+
if (!metadata) {
45+
return false;
46+
}
47+
48+
return metadata['@type'] === OPERATION_METADATA_TYPE_URLS.ExportToYt;
49+
}
50+
51+
export function isImportExportMetadata(
52+
metadata: TOperation['metadata'],
53+
): metadata is ImportFromS3Metadata | ExportToS3Metadata | ExportToYtMetadata {
54+
return (
55+
isImportFromS3Metadata(metadata) ||
56+
isExportToS3Metadata(metadata) ||
57+
isExportToYtMetadata(metadata)
58+
);
59+
}
60+
61+
// i18n keys for import/export progress enum values
62+
// value_progress_unspecified, value_progress_preparing, etc.
63+
export type OperationProgressKey =
64+
| 'value_progress_unspecified'
65+
| 'value_progress_preparing'
66+
| 'value_progress_transfer_data'
67+
| 'value_progress_build_indexes'
68+
| 'value_progress_done'
69+
| 'value_progress_cancellation'
70+
| 'value_progress_cancelled'
71+
| 'value_progress_create_changefeeds';
72+
73+
/**
74+
* Calculate progress percentage from Import/Export metadata
75+
*
76+
* Calculates overall progress based on items_progress array:
77+
* - Sums all parts_total and parts_completed across all items
78+
* - Returns percentage rounded to nearest integer
79+
*
80+
* @param metadata - Import/Export operation metadata
81+
* @returns Progress percentage (0-100) or null if cannot be calculated
82+
*/
83+
export function calculateImportExportProgress(
84+
metadata: ImportFromS3Metadata | ExportToS3Metadata | ExportToYtMetadata | undefined,
85+
): number | null {
86+
if (!metadata?.items_progress || metadata.items_progress.length === 0) {
87+
return null;
88+
}
89+
90+
let totalParts = 0;
91+
let completedParts = 0;
92+
93+
for (const item of metadata.items_progress) {
94+
if (item.parts_total !== undefined && item.parts_total > 0) {
95+
totalParts += item.parts_total;
96+
completedParts += item.parts_completed || 0;
97+
}
98+
}
99+
100+
if (totalParts === 0) {
101+
return null;
102+
}
103+
104+
return Math.round((completedParts / totalParts) * 100);
105+
}
106+
107+
/**
108+
* Get progress display value for an operation
109+
*
110+
* Handles different progress formats:
111+
* - BuildIndex: numeric progress (0-100) -> "75%"
112+
* - Import/Export: calculated from items_progress -> "45%" or enum value -> "Done"
113+
*
114+
* @param operation - Operation to get progress for
115+
* @param translateProgress - Function to translate progress enum values (i18n)
116+
* @returns Formatted progress string or null if no progress available
117+
*/
118+
export function getOperationProgress(
119+
operation: TOperation,
120+
translateProgress: (key: OperationProgressKey) => string,
121+
): string | null {
122+
const metadata = operation.metadata;
123+
124+
if (!metadata) {
125+
return null;
126+
}
127+
128+
if (isIndexBuildMetadata(metadata)) {
129+
if (typeof metadata.progress === 'number') {
130+
return `${Math.round(metadata.progress)}%`;
131+
}
132+
}
133+
134+
// Import/Export: calculate from items_progress or show enum value
135+
if (isImportExportMetadata(metadata)) {
136+
// Try to calculate percentage from items_progress
137+
const calculatedProgress = calculateImportExportProgress(metadata);
138+
if (calculatedProgress !== null) {
139+
return `${calculatedProgress}%`;
140+
}
141+
142+
// Fallback to enum progress value
143+
if (metadata.progress) {
144+
const progressValue =
145+
typeof metadata.progress === 'string'
146+
? metadata.progress
147+
: String(metadata.progress);
148+
149+
// Backend enums are usually PROGRESS_DONE, PROGRESS_PREPARING, etc.
150+
// Normalize by stripping optional PROGRESS_ prefix and lowercasing.
151+
// Both "PROGRESS_DONE" and "DONE" will map to "value_progress_done".
152+
const base = progressValue.replace(/^PROGRESS_/, '').toLowerCase(); // done
153+
const i18nKey = `value_progress_${base}` as OperationProgressKey;
154+
155+
try {
156+
const translated = translateProgress(i18nKey);
157+
if (translated && translated !== i18nKey) {
158+
return translated;
159+
}
160+
} catch {}
161+
162+
return progressValue;
163+
}
164+
}
165+
166+
return null;
167+
}

src/types/api/operations.ts

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,101 @@ export enum IndexBuildState {
8888
STATE_REJECTED = 'STATE_REJECTED',
8989
}
9090

91-
export type TOperationMetadata = IndexBuildMetadata;
91+
export const OPERATION_METADATA_TYPE_URLS = {
92+
IndexBuild: 'type.googleapis.com/Ydb.Table.IndexBuildMetadata',
93+
ImportFromS3: 'type.googleapis.com/Ydb.Import.ImportFromS3Metadata',
94+
ExportToS3: 'type.googleapis.com/Ydb.Export.ExportToS3Metadata',
95+
ExportToYt: 'type.googleapis.com/Ydb.Export.ExportToYtMetadata',
96+
} as const;
97+
98+
export type OperationMetadataTypeUrl =
99+
(typeof OPERATION_METADATA_TYPE_URLS)[keyof typeof OPERATION_METADATA_TYPE_URLS];
100+
101+
/**
102+
* Import/Export progress enum
103+
* source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto
104+
*/
105+
export enum ImportExportProgress {
106+
PROGRESS_UNSPECIFIED = 'PROGRESS_UNSPECIFIED',
107+
PROGRESS_PREPARING = 'PROGRESS_PREPARING',
108+
PROGRESS_TRANSFER_DATA = 'PROGRESS_TRANSFER_DATA',
109+
PROGRESS_BUILD_INDEXES = 'PROGRESS_BUILD_INDEXES',
110+
PROGRESS_DONE = 'PROGRESS_DONE',
111+
PROGRESS_CANCELLATION = 'PROGRESS_CANCELLATION',
112+
PROGRESS_CANCELLED = 'PROGRESS_CANCELLED',
113+
PROGRESS_CREATE_CHANGEFEEDS = 'PROGRESS_CREATE_CHANGEFEEDS',
114+
}
115+
116+
/**
117+
* Import/Export item progress
118+
* source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto
119+
*/
120+
export interface ImportExportItemProgress {
121+
parts_total?: number;
122+
parts_completed?: number;
123+
start_time?: IProtobufTimeObject;
124+
end_time?: IProtobufTimeObject;
125+
}
126+
127+
/**
128+
* Import from S3 metadata
129+
* source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_import.proto#L108
130+
*/
131+
export interface ImportFromS3Metadata {
132+
'@type'?: typeof OPERATION_METADATA_TYPE_URLS.ImportFromS3;
133+
settings?: {
134+
endpoint?: string;
135+
scheme?: string;
136+
bucket?: string;
137+
items?: Array<{
138+
source_prefix?: string;
139+
source_path?: string;
140+
destination_path?: string;
141+
}>;
142+
[key: string]: unknown;
143+
};
144+
progress?: ImportExportProgress | string;
145+
items_progress?: ImportExportItemProgress[];
146+
}
147+
148+
/**
149+
* Export to S3 metadata
150+
* source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_export.proto
151+
*/
152+
export interface ExportToS3Metadata {
153+
'@type'?: typeof OPERATION_METADATA_TYPE_URLS.ExportToS3;
154+
settings?: {
155+
endpoint?: string;
156+
scheme?: string;
157+
bucket?: string;
158+
items?: Array<{
159+
source_path?: string;
160+
destination_prefix?: string;
161+
}>;
162+
[key: string]: unknown;
163+
};
164+
progress?: ImportExportProgress | string;
165+
items_progress?: ImportExportItemProgress[];
166+
}
167+
168+
/**
169+
* Export to YT metadata
170+
* source: https://github.com/ydb-platform/ydb/blob/main/ydb/public/api/protos/ydb_export.proto
171+
*/
172+
export interface ExportToYtMetadata {
173+
'@type'?: typeof OPERATION_METADATA_TYPE_URLS.ExportToYt;
174+
settings?: {
175+
[key: string]: unknown;
176+
};
177+
progress?: ImportExportProgress | string;
178+
items_progress?: ImportExportItemProgress[];
179+
}
180+
181+
export type TOperationMetadata =
182+
| IndexBuildMetadata
183+
| ImportFromS3Metadata
184+
| ExportToS3Metadata
185+
| ExportToYtMetadata;
92186

93187
export interface TCostInfo {
94188
consumed_units?: number;

0 commit comments

Comments
 (0)