diff --git a/src/components/CriticalActionDialog/CriticalActionDialog.tsx b/src/components/CriticalActionDialog/CriticalActionDialog.tsx index ba11153b03..4e80453de8 100644 --- a/src/components/CriticalActionDialog/CriticalActionDialog.tsx +++ b/src/components/CriticalActionDialog/CriticalActionDialog.tsx @@ -3,6 +3,7 @@ import React from 'react'; import {CircleXmarkFill, TriangleExclamationFill} from '@gravity-ui/icons'; import {Checkbox, Dialog, Icon} from '@gravity-ui/uikit'; +import {ResultIssues} from '../../containers/Tenant/Query/Issues/Issues'; import type {IResponseError} from '../../types/api/error'; import {cn} from '../../utils/cn'; @@ -13,6 +14,9 @@ import './CriticalActionDialog.scss'; const b = cn('ydb-critical-dialog'); const parseError = (error: IResponseError) => { + if (error.data && 'issues' in error.data && error.data.issues) { + return ; + } if (error.status === 403) { return criticalActionDialogKeyset('no-rights-error'); } diff --git a/src/containers/Operations/Operations.tsx b/src/containers/Operations/Operations.tsx index bb9d66f54e..d404b1335b 100644 --- a/src/containers/Operations/Operations.tsx +++ b/src/containers/Operations/Operations.tsx @@ -5,7 +5,7 @@ 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 {operationsApi} from '../../store/reducers/operations'; import {useAutoRefreshInterval} from '../../utils/hooks'; import {OperationsControls} from './OperationsControls'; @@ -24,7 +24,7 @@ export function Operations({database}: OperationsProps) { const {kind, searchValue, pageSize, pageToken, handleKindChange, handleSearchChange} = useOperationsQueryParams(); - const {data, isFetching, error} = operationListApi.useGetOperationListQuery( + const {data, isFetching, error, refetch} = operationsApi.useGetOperationListQuery( {database, kind, page_size: pageSize, page_token: pageToken}, { pollingInterval: autoRefreshInterval, @@ -61,7 +61,7 @@ export function Operations({database}: OperationsProps) { {data ? ( diff --git a/src/containers/Operations/OperationsControls.tsx b/src/containers/Operations/OperationsControls.tsx index 1d577cc303..1508054c2a 100644 --- a/src/containers/Operations/OperationsControls.tsx +++ b/src/containers/Operations/OperationsControls.tsx @@ -4,7 +4,7 @@ import {Select} from '@gravity-ui/uikit'; import {EntitiesCount} from '../../components/EntitiesCount'; import {Search} from '../../components/Search'; -import type {OperationKind} from '../../types/api/operationList'; +import type {OperationKind} from '../../types/api/operations'; import {OPERATION_KINDS} from './constants'; import i18n from './i18n'; diff --git a/src/containers/Operations/columns.tsx b/src/containers/Operations/columns.tsx index bf8302d094..fa3843134d 100644 --- a/src/containers/Operations/columns.tsx +++ b/src/containers/Operations/columns.tsx @@ -1,18 +1,30 @@ import {duration} from '@gravity-ui/date-utils'; +import {Ban, CircleStop} from '@gravity-ui/icons'; import type {Column as DataTableColumn} from '@gravity-ui/react-data-table'; -import {Text} from '@gravity-ui/uikit'; +import {ActionTooltip, Flex, Icon, Text} from '@gravity-ui/uikit'; +import {ButtonWithConfirmDialog} from '../../components/ButtonWithConfirmDialog/ButtonWithConfirmDialog'; import {CellWithPopover} from '../../components/CellWithPopover/CellWithPopover'; -import type {TOperation} from '../../types/api/operationList'; -import {EStatusCode} from '../../types/api/operationList'; +import {operationsApi} from '../../store/reducers/operations'; +import type {TOperation} from '../../types/api/operations'; +import {EStatusCode} from '../../types/api/operations'; import {EMPTY_DATA_PLACEHOLDER, HOUR_IN_SECONDS, SECOND_IN_MS} from '../../utils/constants'; +import createToast from '../../utils/createToast'; 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[] { +import './Operations.scss'; + +export function getColumns({ + database, + refreshTable, +}: { + database: string; + refreshTable: VoidFunction; +}): DataTableColumn[] { return [ { name: COLUMNS_NAMES.ID, @@ -114,5 +126,91 @@ export function getColumns(): DataTableColumn[] { return Date.now() - createTime; }, }, + { + name: 'Actions', + sortable: false, + resizeable: false, + header: '', + render: ({row}) => { + return ( + + ); + }, + }, ]; } + +interface OperationsActionsProps { + operation: TOperation; + database: string; + refreshTable: VoidFunction; +} + +function OperationsActions({operation, database, refreshTable}: OperationsActionsProps) { + const [cancelOperation, {isLoading: isLoadingCancel}] = + operationsApi.useCancelOperationMutation(); + const [forgetOperation, {isLoading: isForgetLoading}] = + operationsApi.useForgetOperationMutation(); + + const id = operation.id; + if (!id) { + return null; + } + + return ( + + + + + forgetOperation({id, database}) + .unwrap() + .then(() => { + createToast({ + name: 'Forgotten', + title: i18n('text_forgotten', {id}), + type: 'success', + }); + refreshTable(); + }) + } + buttonDisabled={isLoadingCancel} + > + + + + + + + + cancelOperation({id, database}) + .unwrap() + .then(() => { + createToast({ + name: 'Cancelled', + title: i18n('text_cancelled', {id}), + type: 'success', + }); + refreshTable(); + }) + } + buttonDisabled={isForgetLoading} + > + + + + + + ); +} diff --git a/src/containers/Operations/constants.ts b/src/containers/Operations/constants.ts index 869e54e6dd..67ce34dfbc 100644 --- a/src/containers/Operations/constants.ts +++ b/src/containers/Operations/constants.ts @@ -1,4 +1,4 @@ -import type {OperationKind} from '../../types/api/operationList'; +import type {OperationKind} from '../../types/api/operations'; import i18n from './i18n'; diff --git a/src/containers/Operations/i18n/en.json b/src/containers/Operations/i18n/en.json index d38a15908e..712b4093ab 100644 --- a/src/containers/Operations/i18n/en.json +++ b/src/containers/Operations/i18n/en.json @@ -13,5 +13,12 @@ "column_createTime": "Create Time", "column_endTime": "End Time", "column_duration": "Duration", - "label_duration-ongoing": "{{value}} (ongoing)" + "label_duration-ongoing": "{{value}} (ongoing)", + + "header_cancel": "Cancel operation", + "header_forget": "Forget operation", + "text_cancel": "The operation will be cancelled. Do you want to proceed?", + "text_forget": "The operation will be forgotten. Do you want to proceed?", + "text_forgotten": "The operation {{id}} has been forgotten", + "text_cancelled": "The operation {{id}} has been cancelled" } diff --git a/src/containers/Operations/useOperationsQueryParams.ts b/src/containers/Operations/useOperationsQueryParams.ts index 570cdd044b..af93ff59e0 100644 --- a/src/containers/Operations/useOperationsQueryParams.ts +++ b/src/containers/Operations/useOperationsQueryParams.ts @@ -1,7 +1,7 @@ import {NumberParam, StringParam, useQueryParams} from 'use-query-params'; import {z} from 'zod'; -import type {OperationKind} from '../../types/api/operationList'; +import type {OperationKind} from '../../types/api/operations'; const operationKindSchema = z.enum(['ss/backgrounds', 'export', 'buildindex']).catch('buildindex'); diff --git a/src/containers/Tablets/TabletsTable.tsx b/src/containers/Tablets/TabletsTable.tsx index 646207dfb0..f1bdcaf257 100644 --- a/src/containers/Tablets/TabletsTable.tsx +++ b/src/containers/Tablets/TabletsTable.tsx @@ -136,8 +136,13 @@ function TabletActions(tablet: TTabletStateInfo) { }} buttonDisabled={isDisabledRestart || !isUserAllowedToMakeChanges} withPopover - popoverContent={i18n('controls.kill-not-allowed')} - popoverDisabled={isUserAllowedToMakeChanges} + popoverContent={ + isUserAllowedToMakeChanges + ? i18n('dialog.kill-header') + : i18n('controls.kill-not-allowed') + } + popoverPlacement={['right', 'auto']} + popoverDisabled={false} > diff --git a/src/containers/Tenant/Query/Issues/Issues.tsx b/src/containers/Tenant/Query/Issues/Issues.tsx index a3962a6714..e0c4913d64 100644 --- a/src/containers/Tenant/Query/Issues/Issues.tsx +++ b/src/containers/Tenant/Query/Issues/Issues.tsx @@ -25,9 +25,10 @@ const blockIssue = cn('kv-issue'); interface ResultIssuesProps { data: ErrorResponse | string; + hideSeverity?: boolean; } -export function ResultIssues({data}: ResultIssuesProps) { +export function ResultIssues({data, hideSeverity}: ResultIssuesProps) { const [showIssues, setShowIssues] = React.useState(false); const issues = typeof data === 'string' ? undefined : data?.issues; @@ -41,7 +42,11 @@ export function ResultIssues({data}: ResultIssuesProps) { const severity = getSeverity(data?.error?.severity); content = ( - {' '} + {hideSeverity ? null : ( + + {' '} + + )} {data?.error?.message} @@ -62,15 +67,16 @@ export function ResultIssues({data}: ResultIssuesProps) { )} - {hasIssues && showIssues && } + {hasIssues && showIssues && } ); } interface IssuesProps { issues: IssueMessage[] | null | undefined; + hideSeverity?: boolean; } -export function Issues({issues}: IssuesProps) { +export function Issues({issues, hideSeverity}: IssuesProps) { const mostSevereIssue = issues?.reduce((result, issue) => { const severity = issue.severity ?? 10; return Math.min(result, severity); @@ -78,13 +84,27 @@ export function Issues({issues}: IssuesProps) { return ( {issues?.map((issue, index) => ( - + ))} ); } -function Issue({issue, level = 0}: {issue: IssueMessage; expanded?: boolean; level?: number}) { +function Issue({ + issue, + hideSeverity, + level = 0, +}: { + issue: IssueMessage; + expanded?: boolean; + hideSeverity?: boolean; + level?: number; +}) { const [isExpand, setIsExpand] = React.useState(true); const severity = getSeverity(issue.severity); const position = getIssuePosition(issue); @@ -111,7 +131,7 @@ function Issue({issue, level = 0}: {issue: IssueMessage; expanded?: boolean; lev )} - + {hideSeverity ? null : } {position && ( diff --git a/src/services/api.ts b/src/services/api.ts index d55d597944..df8d7d0a98 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -23,7 +23,12 @@ import type {ModifyDiskResponse} from '../types/api/modifyDisk'; import type {TNetInfo} from '../types/api/netInfo'; import type {NodesRequestParams, TNodesInfo} from '../types/api/nodes'; import type {TEvNodesInfo} from '../types/api/nodesList'; -import type {OperationListRequestParams, TOperationList} from '../types/api/operationList'; +import type { + OperationCancelRequestParams, + OperationForgetRequestParams, + OperationListRequestParams, + TOperationList, +} from '../types/api/operations'; import type {EDecommitStatus, TEvPDiskStateResponse, TPDiskInfoResponse} from '../types/api/pdisk'; import type { Actions, @@ -887,6 +892,36 @@ export class YdbEmbeddedAPI extends AxiosWrapper { ); } + cancelOperation( + params: OperationCancelRequestParams, + {concurrentId, signal}: AxiosOptions = {}, + ) { + return this.post( + this.getPath('/operation/cancel'), + {}, + {...params}, + { + concurrentId, + requestConfig: {signal}, + }, + ); + } + + forgetOperation( + params: OperationForgetRequestParams, + {concurrentId, signal}: AxiosOptions = {}, + ) { + return this.post( + this.getPath('/operation/forget'), + {}, + {...params}, + { + concurrentId, + requestConfig: {signal}, + }, + ); + } + getClusterBaseInfo( _clusterName: string, _opts: AxiosOptions = {}, diff --git a/src/store/reducers/operationList.ts b/src/store/reducers/operationList.ts deleted file mode 100644 index 88daff9b7d..0000000000 --- a/src/store/reducers/operationList.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type {OperationListRequestParams} from '../../types/api/operationList'; - -import {api} from './api'; - -export const operationListApi = api.injectEndpoints({ - endpoints: (build) => ({ - getOperationList: build.query({ - queryFn: async (params: OperationListRequestParams, {signal}) => { - try { - const data = await window.api.getOperationList(params, {signal}); - return {data}; - } catch (error) { - return {error}; - } - }, - providesTags: ['All'], - }), - }), - overrideExisting: 'throw', -}); diff --git a/src/store/reducers/operations.ts b/src/store/reducers/operations.ts new file mode 100644 index 0000000000..f1fed41477 --- /dev/null +++ b/src/store/reducers/operations.ts @@ -0,0 +1,44 @@ +import type { + OperationCancelRequestParams, + OperationForgetRequestParams, + OperationListRequestParams, +} from '../../types/api/operations'; + +import {api} from './api'; + +export const operationsApi = api.injectEndpoints({ + endpoints: (build) => ({ + getOperationList: build.query({ + queryFn: async (params: OperationListRequestParams, {signal}) => { + try { + const data = await window.api.getOperationList(params, {signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + providesTags: ['All'], + }), + cancelOperation: build.mutation({ + queryFn: async (params: OperationCancelRequestParams, {signal}) => { + try { + const data = await window.api.cancelOperation(params, {signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + }), + forgetOperation: build.mutation({ + queryFn: async (params: OperationForgetRequestParams, {signal}) => { + try { + const data = await window.api.forgetOperation(params, {signal}); + return {data}; + } catch (error) { + return {error}; + } + }, + }), + }), + overrideExisting: 'throw', +}); diff --git a/src/types/api/error.ts b/src/types/api/error.ts index ed6bea8f6b..3b7086745d 100644 --- a/src/types/api/error.ts +++ b/src/types/api/error.ts @@ -1,4 +1,9 @@ -export interface IResponseError { +import type {TIssueMessage} from './operations'; + +// TODO: extend with other error types +type ResponseErrorData = TIssueMessage; + +export interface IResponseError { data?: T; status?: number; statusText?: string; diff --git a/src/types/api/operationList.ts b/src/types/api/operations.ts similarity index 95% rename from src/types/api/operationList.ts rename to src/types/api/operations.ts index 5f5c3bd7b0..9c68b35a2f 100644 --- a/src/types/api/operationList.ts +++ b/src/types/api/operations.ts @@ -121,3 +121,13 @@ export interface OperationListRequestParams { page_size?: number; page_token?: string; } + +export interface OperationCancelRequestParams { + database: string; + id: string; +} + +export interface OperationForgetRequestParams { + database: string; + id: string; +}