Skip to content

Commit 3393edf

Browse files
committed
feat: display a list of operations
1 parent d254ee2 commit 3393edf

File tree

16 files changed

+665
-0
lines changed

16 files changed

+665
-0
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.operations {
2+
&__table {
3+
.g-tooltip {
4+
// stylelint-disable-next-line declaration-no-important
5+
height: var(--g-text-body-2-line-height) !important;
6+
}
7+
}
8+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React from 'react';
2+
3+
import {AccessDenied} from '../../components/Errors/403';
4+
import {isAccessError} from '../../components/Errors/PageError/PageError';
5+
import {ResponseError} from '../../components/Errors/ResponseError';
6+
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
7+
import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout';
8+
import {operationListApi} from '../../store/reducers/operationList';
9+
import {useAutoRefreshInterval} from '../../utils/hooks';
10+
11+
import {OperationsControls} from './OperationsControls';
12+
import {getColumns} from './columns';
13+
import {BASE_COLUMNS, BUILD_INDEX_COLUMNS} from './constants';
14+
import i18n from './i18n';
15+
import {b} from './shared';
16+
import {useOperationsQueryParams} from './useOperationsQueryParams';
17+
18+
import './Operations.scss';
19+
20+
interface OperationsProps {
21+
database: string;
22+
}
23+
24+
export function Operations({database}: OperationsProps) {
25+
const [autoRefreshInterval] = useAutoRefreshInterval();
26+
27+
const {kind, searchValue, pageSize, pageToken, handleKindChange, handleSearchChange} =
28+
useOperationsQueryParams();
29+
30+
const {data, isFetching, error} = operationListApi.useGetOperationListQuery(
31+
{database, kind, page_size: pageSize, page_token: pageToken},
32+
{
33+
pollingInterval: autoRefreshInterval,
34+
},
35+
);
36+
37+
const filteredOperations = React.useMemo(() => {
38+
if (!data?.operations) {
39+
return [];
40+
}
41+
return data.operations.filter((op) =>
42+
op.id?.toLowerCase().includes(searchValue.toLowerCase()),
43+
);
44+
}, [data?.operations, searchValue]);
45+
46+
if (isAccessError(error)) {
47+
return <AccessDenied position="left" />;
48+
}
49+
50+
const columnsList: string[] = BASE_COLUMNS;
51+
52+
if (kind === 'buildindex') {
53+
columnsList.push(...BUILD_INDEX_COLUMNS);
54+
}
55+
56+
const columns = getColumns().filter(({name}) => columnsList.includes(name));
57+
58+
return (
59+
<TableWithControlsLayout>
60+
<TableWithControlsLayout.Controls>
61+
<OperationsControls
62+
kind={kind}
63+
searchValue={searchValue}
64+
entitiesCountCurrent={filteredOperations.length}
65+
entitiesCountTotal={data?.operations?.length}
66+
entitiesLoading={isFetching}
67+
handleKindChange={handleKindChange}
68+
handleSearchChange={handleSearchChange}
69+
/>
70+
</TableWithControlsLayout.Controls>
71+
{error ? <ResponseError error={error} /> : null}
72+
<TableWithControlsLayout.Table loading={isFetching} className={b('table')}>
73+
{data ? (
74+
<ResizeableDataTable
75+
columns={columns}
76+
data={filteredOperations}
77+
emptyDataMessage={i18n('operations.noData')}
78+
/>
79+
) : null}
80+
</TableWithControlsLayout.Table>
81+
</TableWithControlsLayout>
82+
);
83+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
3+
import {Select} from '@gravity-ui/uikit';
4+
5+
import {EntitiesCount} from '../../components/EntitiesCount';
6+
import {Search} from '../../components/Search';
7+
import type {OperationKind} from '../../types/api/operationList';
8+
9+
import {OPERATION_KINDS} from './constants';
10+
import i18n from './i18n';
11+
import {b} from './shared';
12+
13+
interface OperationsControlsProps {
14+
kind: OperationKind;
15+
searchValue: string;
16+
entitiesCountCurrent: number;
17+
entitiesCountTotal?: number;
18+
entitiesLoading: boolean;
19+
handleKindChange: (kind: OperationKind) => void;
20+
handleSearchChange: (value: string) => void;
21+
}
22+
23+
export function OperationsControls({
24+
kind,
25+
searchValue,
26+
entitiesCountCurrent,
27+
entitiesCountTotal,
28+
entitiesLoading,
29+
handleKindChange,
30+
handleSearchChange,
31+
}: OperationsControlsProps) {
32+
return (
33+
<React.Fragment>
34+
<Search
35+
value={searchValue}
36+
onChange={handleSearchChange}
37+
placeholder={i18n('controls.searchPlaceholder')}
38+
className={b('search')}
39+
/>
40+
<Select
41+
value={[kind]}
42+
options={OPERATION_KINDS}
43+
onUpdate={(value) => handleKindChange(value[0] as OperationKind)}
44+
/>
45+
<EntitiesCount
46+
label={i18n('label.operations')}
47+
loading={entitiesLoading}
48+
total={entitiesCountTotal}
49+
current={entitiesCountCurrent}
50+
/>
51+
</React.Fragment>
52+
);
53+
}
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import type {Column as DataTableColumn} from '@gravity-ui/react-data-table';
2+
import {Text} from '@gravity-ui/uikit';
3+
4+
import {EntityStatus} from '../../components/EntityStatus/EntityStatus';
5+
import type {IndexBuildMetadata, TOperation} from '../../types/api/operationList';
6+
import {EStatusCode} from '../../types/api/operationList';
7+
import {EMPTY_DATA_PLACEHOLDER} from '../../utils/constants';
8+
9+
import {COLUMNS_NAMES, COLUMNS_TITLES} from './constants';
10+
import i18n from './i18n';
11+
12+
export function getColumns(): DataTableColumn<TOperation>[] {
13+
return [
14+
{
15+
name: COLUMNS_NAMES.ID,
16+
header: COLUMNS_TITLES[COLUMNS_NAMES.ID],
17+
width: 220,
18+
render: ({row}) => {
19+
if (!row.id) {
20+
return EMPTY_DATA_PLACEHOLDER;
21+
}
22+
return <EntityStatus name={row.id} showStatus={false} />;
23+
},
24+
},
25+
{
26+
name: COLUMNS_NAMES.STATUS,
27+
header: COLUMNS_TITLES[COLUMNS_NAMES.STATUS],
28+
render: ({row}) => {
29+
if (!row.status) {
30+
return EMPTY_DATA_PLACEHOLDER;
31+
}
32+
return (
33+
<Text color={row.status === EStatusCode.SUCCESS ? 'positive' : 'danger'}>
34+
{row.status}
35+
</Text>
36+
);
37+
},
38+
},
39+
{
40+
name: COLUMNS_NAMES.CREATED_BY,
41+
header: COLUMNS_TITLES[COLUMNS_NAMES.CREATED_BY],
42+
render: ({row}) => {
43+
if (!row.created_by) {
44+
return EMPTY_DATA_PLACEHOLDER;
45+
}
46+
return row.created_by;
47+
},
48+
},
49+
{
50+
name: COLUMNS_NAMES.CREATE_TIME,
51+
header: COLUMNS_TITLES[COLUMNS_NAMES.CREATE_TIME],
52+
render: ({row}) => {
53+
if (!row.create_time) {
54+
return EMPTY_DATA_PLACEHOLDER;
55+
}
56+
return new Date(row.create_time || '').toLocaleString();
57+
},
58+
sortAccessor: (row) => new Date(row.create_time || '').getTime(),
59+
},
60+
{
61+
name: COLUMNS_NAMES.END_TIME,
62+
header: COLUMNS_TITLES[COLUMNS_NAMES.END_TIME],
63+
render: ({row}) => {
64+
if (!row.end_time) {
65+
return EMPTY_DATA_PLACEHOLDER;
66+
}
67+
return row.end_time ? new Date(row.end_time).toLocaleString() : '-';
68+
},
69+
sortAccessor: (row) =>
70+
row.end_time ? new Date(row.end_time).getTime() : Number.MAX_SAFE_INTEGER,
71+
},
72+
{
73+
name: COLUMNS_NAMES.DURATION,
74+
header: COLUMNS_TITLES[COLUMNS_NAMES.DURATION],
75+
render: ({row}) => {
76+
if (!row.create_time) {
77+
return EMPTY_DATA_PLACEHOLDER;
78+
}
79+
if (row.create_time && row.end_time) {
80+
const duration =
81+
new Date(row.end_time).getTime() - new Date(row.create_time).getTime();
82+
return `${(duration / 1000).toFixed(2)}s`;
83+
}
84+
const duration = Date.now() - new Date(row.create_time || '').getTime();
85+
return `${(duration / 1000).toFixed(2)}s (ongoing)`;
86+
},
87+
sortAccessor: (row) => {
88+
if (row.create_time && row.end_time) {
89+
return new Date(row.end_time).getTime() - new Date(row.create_time).getTime();
90+
}
91+
return Date.now() - new Date(row.create_time || '').getTime();
92+
},
93+
},
94+
{
95+
name: COLUMNS_NAMES.INDEX_BUILD_STATE,
96+
header: COLUMNS_TITLES[COLUMNS_NAMES.INDEX_BUILD_STATE],
97+
render: ({row}) => {
98+
const metadata = row.metadata as IndexBuildMetadata;
99+
if (!metadata || !metadata.state) {
100+
return EMPTY_DATA_PLACEHOLDER;
101+
}
102+
return metadata.state;
103+
},
104+
},
105+
{
106+
name: COLUMNS_NAMES.INDEX_BUILD_PROGRESS,
107+
header: COLUMNS_TITLES[COLUMNS_NAMES.INDEX_BUILD_PROGRESS],
108+
render: ({row}) => {
109+
const metadata = row.metadata as IndexBuildMetadata;
110+
if (!metadata || typeof metadata.progress !== 'number') {
111+
return EMPTY_DATA_PLACEHOLDER;
112+
}
113+
return `${metadata.progress}%`;
114+
},
115+
sortAccessor: (row) => {
116+
const metadata = row.metadata as IndexBuildMetadata;
117+
return metadata && typeof metadata.progress === 'number' ? metadata.progress : -1;
118+
},
119+
},
120+
{
121+
name: COLUMNS_NAMES.INDEX_NAME,
122+
header: COLUMNS_TITLES[COLUMNS_NAMES.INDEX_NAME],
123+
render: ({row}) => {
124+
const metadata = row.metadata as IndexBuildMetadata;
125+
if (!metadata || !metadata.description || !metadata.description.index) {
126+
return EMPTY_DATA_PLACEHOLDER;
127+
}
128+
return metadata.description.index.name || EMPTY_DATA_PLACEHOLDER;
129+
},
130+
},
131+
{
132+
name: COLUMNS_NAMES.INDEX_TYPE,
133+
header: COLUMNS_TITLES[COLUMNS_NAMES.INDEX_TYPE],
134+
render: ({row}) => {
135+
const metadata = row.metadata as IndexBuildMetadata;
136+
if (!metadata || !metadata.description || !metadata.description.index) {
137+
return EMPTY_DATA_PLACEHOLDER;
138+
}
139+
const index = metadata.description.index;
140+
if ('global_index' in index) {
141+
return i18n('indexType.globalIndex');
142+
}
143+
if ('global_async_index' in index) {
144+
return i18n('indexType.globalAsyncIndex');
145+
}
146+
if ('global_unique_index' in index) {
147+
return i18n('indexType.globalUniqueIndex');
148+
}
149+
if ('global_vector_kmeans_tree_index' in index) {
150+
return i18n('indexType.globalVectorKMeansTreeIndex');
151+
}
152+
return EMPTY_DATA_PLACEHOLDER;
153+
},
154+
},
155+
{
156+
name: COLUMNS_NAMES.INDEX_PATH,
157+
header: COLUMNS_TITLES[COLUMNS_NAMES.INDEX_PATH],
158+
render: ({row}) => {
159+
const metadata = row.metadata as IndexBuildMetadata;
160+
if (!metadata || !metadata.description || !metadata.description.path) {
161+
return EMPTY_DATA_PLACEHOLDER;
162+
}
163+
return metadata.description.path;
164+
},
165+
},
166+
{
167+
name: COLUMNS_NAMES.INDEX_COLUMNS,
168+
header: COLUMNS_TITLES[COLUMNS_NAMES.INDEX_COLUMNS],
169+
render: ({row}) => {
170+
const metadata = row.metadata as IndexBuildMetadata;
171+
if (
172+
!metadata ||
173+
!metadata.description ||
174+
!metadata.description.index ||
175+
!metadata.description.index.index_columns
176+
) {
177+
return EMPTY_DATA_PLACEHOLDER;
178+
}
179+
180+
return metadata.description.index.index_columns.join(', ');
181+
},
182+
},
183+
];
184+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type {OperationKind} from '../../types/api/operationList';
2+
3+
import i18n from './i18n';
4+
5+
export const OPERATIONS_SELECTED_COLUMNS_KEY = 'selectedOperationColumns';
6+
7+
export const COLUMNS_NAMES = {
8+
ID: 'id',
9+
STATUS: 'status',
10+
CREATED_BY: 'created_by',
11+
CREATE_TIME: 'create_time',
12+
END_TIME: 'end_time',
13+
DURATION: 'duration',
14+
INDEX_BUILD_STATE: 'index_build_state',
15+
INDEX_BUILD_PROGRESS: 'index_build_progress',
16+
INDEX_NAME: 'index_name',
17+
INDEX_TYPE: 'index_type',
18+
INDEX_PATH: 'index_path',
19+
INDEX_COLUMNS: 'index_columns',
20+
} as const;
21+
22+
export const COLUMNS_TITLES = {
23+
[COLUMNS_NAMES.ID]: i18n('column.operationId'),
24+
[COLUMNS_NAMES.STATUS]: i18n('column.status'),
25+
[COLUMNS_NAMES.CREATED_BY]: i18n('column.createdBy'),
26+
[COLUMNS_NAMES.CREATE_TIME]: i18n('column.createTime'),
27+
[COLUMNS_NAMES.END_TIME]: i18n('column.endTime'),
28+
[COLUMNS_NAMES.DURATION]: i18n('column.duration'),
29+
[COLUMNS_NAMES.INDEX_BUILD_STATE]: i18n('column.indexBuildState'),
30+
[COLUMNS_NAMES.INDEX_BUILD_PROGRESS]: i18n('column.indexBuildProgress'),
31+
[COLUMNS_NAMES.INDEX_NAME]: i18n('column.indexName'),
32+
[COLUMNS_NAMES.INDEX_TYPE]: i18n('column.indexType'),
33+
[COLUMNS_NAMES.INDEX_PATH]: i18n('column.indexPath'),
34+
[COLUMNS_NAMES.INDEX_COLUMNS]: i18n('column.indexColumns'),
35+
} as const;
36+
37+
export const BASE_COLUMNS = [
38+
COLUMNS_NAMES.ID,
39+
COLUMNS_NAMES.STATUS,
40+
COLUMNS_NAMES.CREATED_BY,
41+
COLUMNS_NAMES.CREATE_TIME,
42+
COLUMNS_NAMES.END_TIME,
43+
COLUMNS_NAMES.DURATION,
44+
];
45+
46+
export const BUILD_INDEX_COLUMNS = [
47+
COLUMNS_NAMES.INDEX_BUILD_STATE,
48+
COLUMNS_NAMES.INDEX_BUILD_PROGRESS,
49+
COLUMNS_NAMES.INDEX_NAME,
50+
COLUMNS_NAMES.INDEX_TYPE,
51+
COLUMNS_NAMES.INDEX_PATH,
52+
COLUMNS_NAMES.INDEX_COLUMNS,
53+
];
54+
55+
export const OPERATION_KINDS: {value: OperationKind; content: string}[] = [
56+
{value: 'ss/backgrounds', content: i18n('kind.ssBackgrounds')},
57+
{value: 'export', content: i18n('kind.export')},
58+
{value: 'buildindex', content: i18n('kind.buildIndex')},
59+
];

0 commit comments

Comments
 (0)