Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 39 additions & 28 deletions src/containers/Operations/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ import {parseProtobufTimestampToMs} from '../../utils/timeParsers';

import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants';
import i18n from './i18n';
import {getOperationProgress, isIndexBuildMetadata} from './utils';

import './Operations.scss';

const IMPORT_EXPORT_KINDS: OperationKind[] = ['import/s3', 'export/s3', 'export/yt'];

export function getColumns({
database,
refreshTable,
Expand All @@ -28,13 +31,17 @@ export function getColumns({
kind: OperationKind;
}): DataTableColumn<TOperation>[] {
const isBuildIndex = kind === 'buildindex';
const isImportOrExport = IMPORT_EXPORT_KINDS.includes(kind);

// Helper function to get description tooltip content
// Helper function to get description tooltip content (buildindex-only)
const getDescriptionTooltip = (operation: TOperation): string => {
if (!operation.metadata?.description) {
const {metadata} = operation;

if (!isIndexBuildMetadata(metadata) || !metadata.description) {
return '';
}
return JSON.stringify(operation.metadata.description, null, 2);

return JSON.stringify(metadata.description, null, 2);
};

const columns: DataTableColumn<TOperation>[] = [
Expand Down Expand Up @@ -72,34 +79,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, 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,
Expand Down
8 changes: 8 additions & 0 deletions src/containers/Operations/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,14 @@
"column_state": "State",
"column_progress": "Progress",
"label_duration-ongoing": "{{value}} (ongoing)",
"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?",
Expand Down
167 changes: 167 additions & 0 deletions src/containers/Operations/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
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.
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
*
* 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: OperationProgressKey) => string,
): string | null {
const metadata = operation.metadata;

if (!metadata) {
return null;
}

if (isIndexBuildMetadata(metadata)) {
if (typeof metadata.progress === 'number') {
return `${Math.round(metadata.progress)}%`;
}
}

// Import/Export: calculate from items_progress or show enum value
if (isImportExportMetadata(metadata)) {
// Try to calculate percentage from items_progress
const calculatedProgress = calculateImportExportProgress(metadata);
if (calculatedProgress !== null) {
return `${calculatedProgress}%`;
}

// Fallback to enum progress value
if (metadata.progress) {
const progressValue =
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.
// 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);
if (translated && translated !== i18nKey) {
return translated;
}
} catch {}

return progressValue;
}
}

return null;
}
96 changes: 95 additions & 1 deletion src/types/api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,101 @@ export enum IndexBuildState {
STATE_REJECTED = 'STATE_REJECTED',
}

export type TOperationMetadata = IndexBuildMetadata;
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 {
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'?: typeof OPERATION_METADATA_TYPE_URLS.ImportFromS3;
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'?: typeof OPERATION_METADATA_TYPE_URLS.ExportToS3;
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'?: typeof OPERATION_METADATA_TYPE_URLS.ExportToYt;
settings?: {
[key: string]: unknown;
};
progress?: ImportExportProgress | string;
items_progress?: ImportExportItemProgress[];
}

export type TOperationMetadata =
| IndexBuildMetadata
| ImportFromS3Metadata
| ExportToS3Metadata
| ExportToYtMetadata;

export interface TCostInfo {
consumed_units?: number;
Expand Down
Loading