Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
69 changes: 39 additions & 30 deletions src/containers/Operations/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -28,13 +29,14 @@ export function getColumns({
kind: OperationKind;
}): DataTableColumn<TOperation>[] {
const isBuildIndex = kind === 'buildindex';
const isImportOrExport = ['import/s3', 'export/s3', 'export/yt'].includes(kind);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe take constant out of function?


// Helper function to get description tooltip content
const getDescriptionTooltip = (operation: TOperation): string => {
if (!operation.metadata?.description) {
// Helper function to get description tooltip content (buildindex-only)
const getDescriptionTooltip = (metadata?: IndexBuildMetadata): string => {
if (!metadata?.description) {
return '';
}
return JSON.stringify(operation.metadata.description, null, 2);
return JSON.stringify(metadata.description, null, 2);
};

const columns: DataTableColumn<TOperation>[] = [
Expand All @@ -47,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) ||
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here and farther is it possible to get rid of typecast?

row.id
: row.id;

return (
<CellWithPopover placement={['top', 'bottom']} content={tooltipContent}>
Expand All @@ -72,34 +77,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
118 changes: 118 additions & 0 deletions src/containers/Operations/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type {
ExportToS3Metadata,
ExportToYtMetadata,
ImportFromS3Metadata,
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
*
* 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 (metadata['@type'] === 'type.googleapis.com/Ydb.Table.IndexBuildMetadata') {
const buildIndexMetadata = metadata;
if (typeof buildIndexMetadata.progress === 'number') {
return `${Math.round(buildIndexMetadata.progress)}%`;
}
}

// 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'
) {
const importExportMetadata = metadata;

// 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);

const normalized = progressValue.toLowerCase(); // progress_done
const i18nKey = `value_${normalized}` as OperationProgressKey;

try {
const translated = translateProgress(i18nKey);
if (translated && translated !== i18nKey) {
return translated;
}
} catch {}

return progressValue;
}
}

return null;
}
91 changes: 90 additions & 1 deletion src/types/api/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading