Skip to content

Commit

Permalink
feat: display a list of operations (#1445)
Browse files Browse the repository at this point in the history
  • Loading branch information
astandrik authored Oct 18, 2024
1 parent 8405466 commit 3dda3fe
Show file tree
Hide file tree
Showing 15 changed files with 525 additions and 0 deletions.
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

0 comments on commit 3dda3fe

Please sign in to comment.