Skip to content

Commit b31d2cf

Browse files
authored
feat: add control to execute forget command for an export (#1552)
1 parent 74b27f7 commit b31d2cf

File tree

14 files changed

+250
-42
lines changed

14 files changed

+250
-42
lines changed

src/components/CriticalActionDialog/CriticalActionDialog.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import React from 'react';
33
import {CircleXmarkFill, TriangleExclamationFill} from '@gravity-ui/icons';
44
import {Checkbox, Dialog, Icon} from '@gravity-ui/uikit';
55

6+
import {ResultIssues} from '../../containers/Tenant/Query/Issues/Issues';
67
import type {IResponseError} from '../../types/api/error';
78
import {cn} from '../../utils/cn';
89

@@ -13,6 +14,9 @@ import './CriticalActionDialog.scss';
1314
const b = cn('ydb-critical-dialog');
1415

1516
const parseError = (error: IResponseError) => {
17+
if (error.data && 'issues' in error.data && error.data.issues) {
18+
return <ResultIssues hideSeverity data={error.data} />;
19+
}
1620
if (error.status === 403) {
1721
return criticalActionDialogKeyset('no-rights-error');
1822
}

src/containers/Operations/Operations.tsx

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {isAccessError} from '../../components/Errors/PageError/PageError';
55
import {ResponseError} from '../../components/Errors/ResponseError';
66
import {ResizeableDataTable} from '../../components/ResizeableDataTable/ResizeableDataTable';
77
import {TableWithControlsLayout} from '../../components/TableWithControlsLayout/TableWithControlsLayout';
8-
import {operationListApi} from '../../store/reducers/operationList';
8+
import {operationsApi} from '../../store/reducers/operations';
99
import {useAutoRefreshInterval} from '../../utils/hooks';
1010

1111
import {OperationsControls} from './OperationsControls';
@@ -24,7 +24,7 @@ export function Operations({database}: OperationsProps) {
2424
const {kind, searchValue, pageSize, pageToken, handleKindChange, handleSearchChange} =
2525
useOperationsQueryParams();
2626

27-
const {data, isFetching, error} = operationListApi.useGetOperationListQuery(
27+
const {data, isFetching, error, refetch} = operationsApi.useGetOperationListQuery(
2828
{database, kind, page_size: pageSize, page_token: pageToken},
2929
{
3030
pollingInterval: autoRefreshInterval,
@@ -61,7 +61,7 @@ export function Operations({database}: OperationsProps) {
6161
<TableWithControlsLayout.Table loading={isFetching} className={b('table')}>
6262
{data ? (
6363
<ResizeableDataTable
64-
columns={getColumns()}
64+
columns={getColumns({database, refreshTable: refetch})}
6565
data={filteredOperations}
6666
emptyDataMessage={i18n('title_empty')}
6767
/>

src/containers/Operations/OperationsControls.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {Select} from '@gravity-ui/uikit';
44

55
import {EntitiesCount} from '../../components/EntitiesCount';
66
import {Search} from '../../components/Search';
7-
import type {OperationKind} from '../../types/api/operationList';
7+
import type {OperationKind} from '../../types/api/operations';
88

99
import {OPERATION_KINDS} from './constants';
1010
import i18n from './i18n';

src/containers/Operations/columns.tsx

+102-4
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
import {duration} from '@gravity-ui/date-utils';
2+
import {Ban, CircleStop} from '@gravity-ui/icons';
23
import type {Column as DataTableColumn} from '@gravity-ui/react-data-table';
3-
import {Text} from '@gravity-ui/uikit';
4+
import {ActionTooltip, Flex, Icon, Text} from '@gravity-ui/uikit';
45

6+
import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog';
57
import {CellWithPopover} from '../../components/CellWithPopover/CellWithPopover';
6-
import type {TOperation} from '../../types/api/operationList';
7-
import {EStatusCode} from '../../types/api/operationList';
8+
import {operationsApi} from '../../store/reducers/operations';
9+
import type {TOperation} from '../../types/api/operations';
10+
import {EStatusCode} from '../../types/api/operations';
811
import {EMPTY_DATA_PLACEHOLDER, HOUR_IN_SECONDS, SECOND_IN_MS} from '../../utils/constants';
12+
import createToast from '../../utils/createToast';
913
import {formatDateTime} from '../../utils/dataFormatters/dataFormatters';
1014
import {parseProtobufTimestampToMs} from '../../utils/timeParsers';
1115

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

15-
export function getColumns(): DataTableColumn<TOperation>[] {
19+
import './Operations.scss';
20+
21+
export function getColumns({
22+
database,
23+
refreshTable,
24+
}: {
25+
database: string;
26+
refreshTable: VoidFunction;
27+
}): DataTableColumn<TOperation>[] {
1628
return [
1729
{
1830
name: COLUMNS_NAMES.ID,
@@ -114,5 +126,91 @@ export function getColumns(): DataTableColumn<TOperation>[] {
114126
return Date.now() - createTime;
115127
},
116128
},
129+
{
130+
name: 'Actions',
131+
sortable: false,
132+
resizeable: false,
133+
header: '',
134+
render: ({row}) => {
135+
return (
136+
<OperationsActions
137+
operation={row}
138+
database={database}
139+
refreshTable={refreshTable}
140+
/>
141+
);
142+
},
143+
},
117144
];
118145
}
146+
147+
interface OperationsActionsProps {
148+
operation: TOperation;
149+
database: string;
150+
refreshTable: VoidFunction;
151+
}
152+
153+
function OperationsActions({operation, database, refreshTable}: OperationsActionsProps) {
154+
const [cancelOperation, {isLoading: isLoadingCancel}] =
155+
operationsApi.useCancelOperationMutation();
156+
const [forgetOperation, {isLoading: isForgetLoading}] =
157+
operationsApi.useForgetOperationMutation();
158+
159+
const id = operation.id;
160+
if (!id) {
161+
return null;
162+
}
163+
164+
return (
165+
<Flex gap="2">
166+
<ActionTooltip title={i18n('header_forget')} placement={['left', 'auto']}>
167+
<div>
168+
<ButtonWithConfirmDialog
169+
buttonView="outlined"
170+
dialogHeader={i18n('header_forget')}
171+
dialogText={i18n('text_forget')}
172+
onConfirmAction={() =>
173+
forgetOperation({id, database})
174+
.unwrap()
175+
.then(() => {
176+
createToast({
177+
name: 'Forgotten',
178+
title: i18n('text_forgotten', {id}),
179+
type: 'success',
180+
});
181+
refreshTable();
182+
})
183+
}
184+
buttonDisabled={isLoadingCancel}
185+
>
186+
<Icon data={Ban} />
187+
</ButtonWithConfirmDialog>
188+
</div>
189+
</ActionTooltip>
190+
<ActionTooltip title={i18n('header_cancel')} placement={['right', 'auto']}>
191+
<div>
192+
<ButtonWithConfirmDialog
193+
buttonView="outlined"
194+
dialogHeader={i18n('header_cancel')}
195+
dialogText={i18n('text_cancel')}
196+
onConfirmAction={() =>
197+
cancelOperation({id, database})
198+
.unwrap()
199+
.then(() => {
200+
createToast({
201+
name: 'Cancelled',
202+
title: i18n('text_cancelled', {id}),
203+
type: 'success',
204+
});
205+
refreshTable();
206+
})
207+
}
208+
buttonDisabled={isForgetLoading}
209+
>
210+
<Icon data={CircleStop} />
211+
</ButtonWithConfirmDialog>
212+
</div>
213+
</ActionTooltip>
214+
</Flex>
215+
);
216+
}

src/containers/Operations/constants.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {OperationKind} from '../../types/api/operationList';
1+
import type {OperationKind} from '../../types/api/operations';
22

33
import i18n from './i18n';
44

src/containers/Operations/i18n/en.json

+8-1
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,12 @@
1313
"column_createTime": "Create Time",
1414
"column_endTime": "End Time",
1515
"column_duration": "Duration",
16-
"label_duration-ongoing": "{{value}} (ongoing)"
16+
"label_duration-ongoing": "{{value}} (ongoing)",
17+
18+
"header_cancel": "Cancel operation",
19+
"header_forget": "Forget operation",
20+
"text_cancel": "The operation will be cancelled. Do you want to proceed?",
21+
"text_forget": "The operation will be forgotten. Do you want to proceed?",
22+
"text_forgotten": "The operation {{id}} has been forgotten",
23+
"text_cancelled": "The operation {{id}} has been cancelled"
1724
}

src/containers/Operations/useOperationsQueryParams.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {NumberParam, StringParam, useQueryParams} from 'use-query-params';
22
import {z} from 'zod';
33

4-
import type {OperationKind} from '../../types/api/operationList';
4+
import type {OperationKind} from '../../types/api/operations';
55

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

src/containers/Tablets/TabletsTable.tsx

+7-2
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,13 @@ function TabletActions(tablet: TTabletStateInfo) {
136136
}}
137137
buttonDisabled={isDisabledRestart || !isUserAllowedToMakeChanges}
138138
withPopover
139-
popoverContent={i18n('controls.kill-not-allowed')}
140-
popoverDisabled={isUserAllowedToMakeChanges}
139+
popoverContent={
140+
isUserAllowedToMakeChanges
141+
? i18n('dialog.kill-header')
142+
: i18n('controls.kill-not-allowed')
143+
}
144+
popoverPlacement={['right', 'auto']}
145+
popoverDisabled={false}
141146
>
142147
<Icon data={ArrowRotateLeft} />
143148
</ButtonWithConfirmDialog>

src/containers/Tenant/Query/Issues/Issues.tsx

+27-7
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,10 @@ const blockIssue = cn('kv-issue');
2525

2626
interface ResultIssuesProps {
2727
data: ErrorResponse | string;
28+
hideSeverity?: boolean;
2829
}
2930

30-
export function ResultIssues({data}: ResultIssuesProps) {
31+
export function ResultIssues({data, hideSeverity}: ResultIssuesProps) {
3132
const [showIssues, setShowIssues] = React.useState(false);
3233

3334
const issues = typeof data === 'string' ? undefined : data?.issues;
@@ -41,7 +42,11 @@ export function ResultIssues({data}: ResultIssuesProps) {
4142
const severity = getSeverity(data?.error?.severity);
4243
content = (
4344
<React.Fragment>
44-
<IssueSeverity severity={severity} />{' '}
45+
{hideSeverity ? null : (
46+
<React.Fragment>
47+
<IssueSeverity severity={severity} />{' '}
48+
</React.Fragment>
49+
)}
4550
<span className={blockWrapper('error-message-text')}>
4651
{data?.error?.message}
4752
</span>
@@ -62,29 +67,44 @@ export function ResultIssues({data}: ResultIssuesProps) {
6267
</Button>
6368
)}
6469
</div>
65-
{hasIssues && showIssues && <Issues issues={issues} />}
70+
{hasIssues && showIssues && <Issues hideSeverity={hideSeverity} issues={issues} />}
6671
</div>
6772
);
6873
}
6974

7075
interface IssuesProps {
7176
issues: IssueMessage[] | null | undefined;
77+
hideSeverity?: boolean;
7278
}
73-
export function Issues({issues}: IssuesProps) {
79+
export function Issues({issues, hideSeverity}: IssuesProps) {
7480
const mostSevereIssue = issues?.reduce((result, issue) => {
7581
const severity = issue.severity ?? 10;
7682
return Math.min(result, severity);
7783
}, 10);
7884
return (
7985
<div className={blockIssues(null)}>
8086
{issues?.map((issue, index) => (
81-
<Issue key={index} issue={issue} expanded={issue === mostSevereIssue} />
87+
<Issue
88+
key={index}
89+
hideSeverity={hideSeverity}
90+
issue={issue}
91+
expanded={issue === mostSevereIssue}
92+
/>
8293
))}
8394
</div>
8495
);
8596
}
8697

87-
function Issue({issue, level = 0}: {issue: IssueMessage; expanded?: boolean; level?: number}) {
98+
function Issue({
99+
issue,
100+
hideSeverity,
101+
level = 0,
102+
}: {
103+
issue: IssueMessage;
104+
expanded?: boolean;
105+
hideSeverity?: boolean;
106+
level?: number;
107+
}) {
88108
const [isExpand, setIsExpand] = React.useState(true);
89109
const severity = getSeverity(issue.severity);
90110
const position = getIssuePosition(issue);
@@ -111,7 +131,7 @@ function Issue({issue, level = 0}: {issue: IssueMessage; expanded?: boolean; lev
111131
<ArrowToggle direction={arrowDirection} size={16} />
112132
</Button>
113133
)}
114-
<IssueSeverity severity={severity} />
134+
{hideSeverity ? null : <IssueSeverity severity={severity} />}
115135

116136
<span className={blockIssue('message')}>
117137
{position && (

src/services/api.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@ import type {ModifyDiskResponse} from '../types/api/modifyDisk';
2323
import type {TNetInfo} from '../types/api/netInfo';
2424
import type {NodesRequestParams, TNodesInfo} from '../types/api/nodes';
2525
import type {TEvNodesInfo} from '../types/api/nodesList';
26-
import type {OperationListRequestParams, TOperationList} from '../types/api/operationList';
26+
import type {
27+
OperationCancelRequestParams,
28+
OperationForgetRequestParams,
29+
OperationListRequestParams,
30+
TOperationList,
31+
} from '../types/api/operations';
2732
import type {EDecommitStatus, TEvPDiskStateResponse, TPDiskInfoResponse} from '../types/api/pdisk';
2833
import type {
2934
Actions,
@@ -887,6 +892,36 @@ export class YdbEmbeddedAPI extends AxiosWrapper {
887892
);
888893
}
889894

895+
cancelOperation(
896+
params: OperationCancelRequestParams,
897+
{concurrentId, signal}: AxiosOptions = {},
898+
) {
899+
return this.post<TOperationList>(
900+
this.getPath('/operation/cancel'),
901+
{},
902+
{...params},
903+
{
904+
concurrentId,
905+
requestConfig: {signal},
906+
},
907+
);
908+
}
909+
910+
forgetOperation(
911+
params: OperationForgetRequestParams,
912+
{concurrentId, signal}: AxiosOptions = {},
913+
) {
914+
return this.post<TOperationList>(
915+
this.getPath('/operation/forget'),
916+
{},
917+
{...params},
918+
{
919+
concurrentId,
920+
requestConfig: {signal},
921+
},
922+
);
923+
}
924+
890925
getClusterBaseInfo(
891926
_clusterName: string,
892927
_opts: AxiosOptions = {},

src/store/reducers/operationList.ts

-20
This file was deleted.

0 commit comments

Comments
 (0)