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
72 changes: 72 additions & 0 deletions src/containers/Operations/Operations.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React from 'react';

import {AccessDenied} from '../../components/Errors/403';
import {isAccessError} from '../../components/Errors/PageError/PageError';
import {ResponseError} from '../../components/Errors/ResponseError';
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout';
import {operationListApi} from '../../store/reducers/operationList';
import {useAutoRefreshInterval} from '../../utils/hooks';

import {OperationsControls} from './OperationsControls';
import {getColumns} from './columns';
import i18n from './i18n';
import {b} from './shared';
import {useOperationsQueryParams} from './useOperationsQueryParams';

interface OperationsProps {
database: string;
}

export function Operations({database}: OperationsProps) {
const [autoRefreshInterval] = useAutoRefreshInterval();

const {kind, searchValue, pageSize, pageToken, handleKindChange, handleSearchChange} =
useOperationsQueryParams();

const {data, isFetching, error} = operationListApi.useGetOperationListQuery(
{database, kind, page_size: pageSize, page_token: pageToken},
{
pollingInterval: autoRefreshInterval,
},
);

const filteredOperations = React.useMemo(() => {
if (!data?.operations) {
return [];
}
return data.operations.filter((op) =>
op.id?.toLowerCase().includes(searchValue.toLowerCase()),
);
}, [data?.operations, searchValue]);

if (isAccessError(error)) {
return <AccessDenied position="left" />;
}

return (
<TableWithControlsLayout>
<TableWithControlsLayout.Controls>
<OperationsControls
kind={kind}
searchValue={searchValue}
entitiesCountCurrent={filteredOperations.length}
entitiesCountTotal={data?.operations?.length}
entitiesLoading={isFetching}
handleKindChange={handleKindChange}
handleSearchChange={handleSearchChange}
/>
</TableWithControlsLayout.Controls>
{error ? <ResponseError error={error} /> : null}
<TableWithControlsLayout.Table loading={isFetching} className={b('table')}>
{data ? (
<ResizeableDataTable
columns={getColumns()}
data={filteredOperations}
emptyDataMessage={i18n('title_empty')}
/>
) : null}
</TableWithControlsLayout.Table>
</TableWithControlsLayout>
);
}
53 changes: 53 additions & 0 deletions src/containers/Operations/OperationsControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import React from 'react';

import {Select} from '@gravity-ui/uikit';

import {EntitiesCount} from '../../components/EntitiesCount';
import {Search} from '../../components/Search';
import type {OperationKind} from '../../types/api/operationList';

import {OPERATION_KINDS} from './constants';
import i18n from './i18n';
import {b} from './shared';

interface OperationsControlsProps {
kind: OperationKind;
searchValue: string;
entitiesCountCurrent: number;
entitiesCountTotal?: number;
entitiesLoading: boolean;
handleKindChange: (kind: OperationKind) => void;
handleSearchChange: (value: string) => void;
}

export function OperationsControls({
kind,
searchValue,
entitiesCountCurrent,
entitiesCountTotal,
entitiesLoading,
handleKindChange,
handleSearchChange,
}: OperationsControlsProps) {
return (
<React.Fragment>
<Search
value={searchValue}
onChange={handleSearchChange}
placeholder={i18n('pleaceholder_search')}
className={b('search')}
/>
<Select
value={[kind]}
options={OPERATION_KINDS}
onUpdate={(value) => handleKindChange(value[0] as OperationKind)}
/>
<EntitiesCount
label={i18n('label_operations')}
loading={entitiesLoading}
total={entitiesCountTotal}
current={entitiesCountCurrent}
/>
</React.Fragment>
);
}
118 changes: 118 additions & 0 deletions src/containers/Operations/columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {duration} from '@gravity-ui/date-utils';
import type {Column as DataTableColumn} from '@gravity-ui/react-data-table';
import {Text} from '@gravity-ui/uikit';

import {CellWithPopover} from '../../components/CellWithPopover/CellWithPopover';
import type {TOperation} from '../../types/api/operationList';
import {EStatusCode} from '../../types/api/operationList';
import {EMPTY_DATA_PLACEHOLDER, HOUR_IN_SECONDS, SECOND_IN_MS} from '../../utils/constants';
import {formatDateTime} from '../../utils/dataFormatters/dataFormatters';
import {parseProtobufTimestampToMs} from '../../utils/timeParsers';

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

export function getColumns(): DataTableColumn<TOperation>[] {
return [
{
name: COLUMNS_NAMES.ID,
header: COLUMNS_TITLES[COLUMNS_NAMES.ID],
width: 340,
render: ({row}) => {
if (!row.id) {
return EMPTY_DATA_PLACEHOLDER;
}
return (
<CellWithPopover placement={['top', 'bottom']} content={row.id}>
{row.id}
</CellWithPopover>
);
},
},
{
name: COLUMNS_NAMES.STATUS,
header: COLUMNS_TITLES[COLUMNS_NAMES.STATUS],
render: ({row}) => {
if (!row.status) {
return EMPTY_DATA_PLACEHOLDER;
}
return (
<Text color={row.status === EStatusCode.SUCCESS ? 'positive' : 'danger'}>
{row.status}
</Text>
);
},
},
{
name: COLUMNS_NAMES.CREATED_BY,
header: COLUMNS_TITLES[COLUMNS_NAMES.CREATED_BY],
render: ({row}) => {
if (!row.created_by) {
return EMPTY_DATA_PLACEHOLDER;
}
return row.created_by;
},
},
{
name: COLUMNS_NAMES.CREATE_TIME,
header: COLUMNS_TITLES[COLUMNS_NAMES.CREATE_TIME],
render: ({row}) => {
if (!row.create_time) {
return EMPTY_DATA_PLACEHOLDER;
}
return formatDateTime(parseProtobufTimestampToMs(row.create_time));
},
sortAccessor: (row) =>
row.create_time ? parseProtobufTimestampToMs(row.create_time) : 0,
},
{
name: COLUMNS_NAMES.END_TIME,
header: COLUMNS_TITLES[COLUMNS_NAMES.END_TIME],
render: ({row}) => {
if (!row.end_time) {
return EMPTY_DATA_PLACEHOLDER;
}
return formatDateTime(parseProtobufTimestampToMs(row.end_time));
},
sortAccessor: (row) =>
row.end_time ? parseProtobufTimestampToMs(row.end_time) : Number.MAX_SAFE_INTEGER,
},
{
name: COLUMNS_NAMES.DURATION,
header: COLUMNS_TITLES[COLUMNS_NAMES.DURATION],
render: ({row}) => {
let durationValue = 0;
if (!row.create_time) {
return EMPTY_DATA_PLACEHOLDER;
}
const createTime = parseProtobufTimestampToMs(row.create_time);
if (row.end_time) {
const endTime = parseProtobufTimestampToMs(row.end_time);
durationValue = endTime - createTime;
} else {
durationValue = Date.now() - createTime;
}

const durationFormatted =
durationValue > HOUR_IN_SECONDS * SECOND_IN_MS
? duration(durationValue).format('hh:mm:ss')
: duration(durationValue).format('mm:ss');

return row.end_time
? durationFormatted
: i18n('label_duration-ongoing', {value: durationFormatted});
},
sortAccessor: (row) => {
if (!row.create_time) {
return 0;
}
const createTime = parseProtobufTimestampToMs(row.create_time);
if (row.end_time) {
const endTime = parseProtobufTimestampToMs(row.end_time);
return endTime - createTime;
}
return Date.now() - createTime;
},
},
];
}
38 changes: 38 additions & 0 deletions src/containers/Operations/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type {OperationKind} from '../../types/api/operationList';

import i18n from './i18n';

export const OPERATIONS_SELECTED_COLUMNS_KEY = 'selectedOperationColumns';

export const COLUMNS_NAMES = {
ID: 'id',
STATUS: 'status',
CREATED_BY: 'created_by',
CREATE_TIME: 'create_time',
END_TIME: 'end_time',
DURATION: 'duration',
} as const;

export const COLUMNS_TITLES = {
[COLUMNS_NAMES.ID]: i18n('column_operationId'),
[COLUMNS_NAMES.STATUS]: i18n('column_status'),
[COLUMNS_NAMES.CREATED_BY]: i18n('column_createdBy'),
[COLUMNS_NAMES.CREATE_TIME]: i18n('column_createTime'),
[COLUMNS_NAMES.END_TIME]: i18n('column_endTime'),
[COLUMNS_NAMES.DURATION]: i18n('column_duration'),
} as const;

export const BASE_COLUMNS = [
COLUMNS_NAMES.ID,
COLUMNS_NAMES.STATUS,
COLUMNS_NAMES.CREATED_BY,
COLUMNS_NAMES.CREATE_TIME,
COLUMNS_NAMES.END_TIME,
COLUMNS_NAMES.DURATION,
];

export const OPERATION_KINDS: {value: OperationKind; content: string}[] = [
{value: 'export', content: i18n('kind_export')},
{value: 'ss/backgrounds', content: i18n('kind_ssBackgrounds')},
{value: 'buildindex', content: i18n('kind_buildIndex')},
];
17 changes: 17 additions & 0 deletions src/containers/Operations/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"label_operations": "Operations",
"title_empty": "No operations data",
"pleaceholder_search": "Search operations",
"placeholder_kind": "Select operation kind",
"kind_ssBackgrounds": "SS/Backgrounds",
"kind_export": "Export",
"kind_buildIndex": "Build Index",

"column_operationId": "Operation ID",
"column_status": "Status",
"column_createdBy": "Created By",
"column_createTime": "Create Time",
"column_endTime": "End Time",
"column_duration": "Duration",
"label_duration-ongoing": "{{value}} (ongoing)"
}
7 changes: 7 additions & 0 deletions src/containers/Operations/i18n/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {registerKeysets} from '../../../utils/i18n';

import en from './en.json';

const COMPONENT = 'ydb-operations';

export default registerKeysets(COMPONENT, {en});
1 change: 1 addition & 0 deletions src/containers/Operations/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Operations';
3 changes: 3 additions & 0 deletions src/containers/Operations/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {cn} from '../../utils/cn';

export const b = cn('operations');
47 changes: 47 additions & 0 deletions src/containers/Operations/useOperationsQueryParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import {NumberParam, StringParam, useQueryParams} from 'use-query-params';
import {z} from 'zod';

import type {OperationKind} from '../../types/api/operationList';

const operationKindSchema = z.enum(['ss/backgrounds', 'export', 'buildindex']).catch('buildindex');

export function useOperationsQueryParams() {
const [queryParams, setQueryParams] = useQueryParams({
kind: StringParam,
search: StringParam,
pageSize: NumberParam,
pageToken: StringParam,
});

const kind = operationKindSchema.parse(queryParams.kind) as OperationKind;
const searchValue = queryParams.search ?? '';
const pageSize = queryParams.pageSize ?? undefined;
const pageToken = queryParams.pageToken ?? undefined;

const handleKindChange = (value: OperationKind) => {
setQueryParams({kind: value}, 'replaceIn');
};

const handleSearchChange = (value: string) => {
setQueryParams({search: value || undefined}, 'replaceIn');
};

const handlePageSizeChange = (value: number) => {
setQueryParams({pageSize: value}, 'replaceIn');
};

const handlePageTokenChange = (value: string | undefined) => {
setQueryParams({pageToken: value}, 'replaceIn');
};

return {
kind,
searchValue,
pageSize,
pageToken,
handleKindChange,
handleSearchChange,
handlePageSizeChange,
handlePageTokenChange,
};
}
4 changes: 4 additions & 0 deletions src/containers/Tenant/Diagnostics/Diagnostics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {cn} from '../../../utils/cn';
import {useTypedDispatch, useTypedSelector} from '../../../utils/hooks';
import {Heatmap} from '../../Heatmap';
import {NodesWrapper} from '../../Nodes/NodesWrapper';
import {Operations} from '../../Operations';
import {StorageWrapper} from '../../Storage/StorageWrapper';
import {Tablets} from '../../Tablets';
import {SchemaViewer} from '../Schema/SchemaViewer/SchemaViewer';
Expand Down Expand Up @@ -137,6 +138,9 @@ function Diagnostics(props: DiagnosticsProps) {
case TENANT_DIAGNOSTICS_TABS_IDS.configs: {
return <Configs database={tenantName} />;
}
case TENANT_DIAGNOSTICS_TABS_IDS.operations: {
return <Operations database={tenantName} />;
}
default: {
return <div>No data...</div>;
}
Expand Down
Loading
Loading