From 50639cabb8135d19325bb38f24e59de77c1fce04 Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Wed, 23 Dec 2020 06:59:44 -0800 Subject: [PATCH] Add cancel action to ActiveTasksTable --- main.go | 3 +- ui/src/actions/tasksActions.ts | 93 +++++++++++++++++++++ ui/src/api.ts | 26 ++++++ ui/src/components/ActiveTasksTable.tsx | 29 +++++-- ui/src/components/TableActions.tsx | 5 +- ui/src/reducers/snackbarReducer.ts | 18 +++++ ui/src/reducers/tasksReducer.ts | 108 +++++++++++++++++++++++++ 7 files changed, 274 insertions(+), 8 deletions(-) diff --git a/main.go b/main.go index 14a96a4..24f9ba7 100644 --- a/main.go +++ b/main.go @@ -78,8 +78,7 @@ func main() { // Task endpoints. api.HandleFunc("/queues/{qname}/active_tasks", newListActiveTasksHandlerFunc(inspector)).Methods("GET") api.HandleFunc("/queues/{qname}/active_tasks/{task_id}:cancel", newCancelActiveTaskHandlerFunc(inspector)).Methods("POST") - api.HandleFunc("/queues/{qname}/active_tasks/:cancel_all", newCancelAllActiveTasksHandlerFunc(inspector)).Methods("POST") - api.HandleFunc("/queues/{qname}/active_tasks:batch_cancel", newBatchCancelActiveTasksHandlerFunc(inspector)).Methods("POST") + api.HandleFunc("/queues/{qname}/active_tasks:cancel_all", newCancelAllActiveTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/active_tasks:batch_cancel", newBatchCancelActiveTasksHandlerFunc(inspector)).Methods("POST") api.HandleFunc("/queues/{qname}/pending_tasks", newListPendingTasksHandlerFunc(inspector)).Methods("GET") api.HandleFunc("/queues/{qname}/scheduled_tasks", newListScheduledTasksHandlerFunc(inspector)).Methods("GET") diff --git a/ui/src/actions/tasksActions.ts b/ui/src/actions/tasksActions.ts index c76148d..e9ed28d 100644 --- a/ui/src/actions/tasksActions.ts +++ b/ui/src/actions/tasksActions.ts @@ -1,4 +1,6 @@ import { + batchCancelActiveTasks, + BatchCancelTasksResponse, batchDeleteDeadTasks, batchDeleteRetryTasks, batchDeleteScheduledTasks, @@ -11,6 +13,7 @@ import { batchRunScheduledTasks, BatchRunTasksResponse, cancelActiveTask, + cancelAllActiveTasks, deleteAllDeadTasks, deleteAllRetryTasks, deleteAllScheduledTasks, @@ -60,6 +63,16 @@ export const LIST_DEAD_TASKS_ERROR = "LIST_DEAD_TASKS_ERROR"; export const CANCEL_ACTIVE_TASK_BEGIN = "CANCEL_ACTIVE_TASK_BEGIN"; export const CANCEL_ACTIVE_TASK_SUCCESS = "CANCEL_ACTIVE_TASK_SUCCESS"; export const CANCEL_ACTIVE_TASK_ERROR = "CANCEL_ACTIVE_TASK_ERROR"; +export const CANCEL_ALL_ACTIVE_TASKS_BEGIN = "CANCEL_ALL_ACTIVE_TASKS_BEGIN"; +export const CANCEL_ALL_ACTIVE_TASKS_SUCCESS = + "CANCEL_ALL_ACTIVE_TASKS_SUCCESS"; +export const CANCEL_ALL_ACTIVE_TASKS_ERROR = "CANCEL_ALL_ACTIVE_TASKS_ERROR"; +export const BATCH_CANCEL_ACTIVE_TASKS_BEGIN = + "BATCH_CANCEL_ACTIVE_TASKS_BEGIN"; +export const BATCH_CANCEL_ACTIVE_TASKS_SUCCESS = + "BATCH_CANCEL_ACTIVE_TASKS_SUCCESS"; +export const BATCH_CANCEL_ACTIVE_TASKS_ERROR = + "BATCH_CANCEL_ACTIVE_TASKS_ERROR"; export const RUN_SCHEDULED_TASK_BEGIN = "RUN_DEAD_TASK_BEGIN"; export const RUN_SCHEDULED_TASK_SUCCESS = "RUN_DEAD_TASK_SUCCESS"; export const RUN_SCHEDULED_TASK_ERROR = "RUN_DEAD_TASK_ERROR"; @@ -253,6 +266,41 @@ interface CancelActiveTaskErrorAction { error: string; } +interface CancelAllActiveTasksBeginAction { + type: typeof CANCEL_ALL_ACTIVE_TASKS_BEGIN; + queue: string; +} + +interface CancelAllActiveTasksSuccessAction { + type: typeof CANCEL_ALL_ACTIVE_TASKS_SUCCESS; + queue: string; +} + +interface CancelAllActiveTasksErrorAction { + type: typeof CANCEL_ALL_ACTIVE_TASKS_ERROR; + queue: string; + error: string; +} + +interface BatchCancelActiveTasksBeginAction { + type: typeof BATCH_CANCEL_ACTIVE_TASKS_BEGIN; + queue: string; + taskIds: string[]; +} + +interface BatchCancelActiveTasksSuccessAction { + type: typeof BATCH_CANCEL_ACTIVE_TASKS_SUCCESS; + queue: string; + payload: BatchCancelTasksResponse; +} + +interface BatchCancelActiveTasksErrorAction { + type: typeof BATCH_CANCEL_ACTIVE_TASKS_ERROR; + queue: string; + taskIds: string[]; + error: string; +} + interface RunScheduledTaskBeginAction { type: typeof RUN_SCHEDULED_TASK_BEGIN; queue: string; @@ -705,6 +753,12 @@ export type TasksActionTypes = | CancelActiveTaskBeginAction | CancelActiveTaskSuccessAction | CancelActiveTaskErrorAction + | CancelAllActiveTasksBeginAction + | CancelAllActiveTasksSuccessAction + | CancelAllActiveTasksErrorAction + | BatchCancelActiveTasksBeginAction + | BatchCancelActiveTasksSuccessAction + | BatchCancelActiveTasksErrorAction | RunScheduledTaskBeginAction | RunScheduledTaskSuccessAction | RunScheduledTaskErrorAction @@ -910,6 +964,45 @@ export function cancelActiveTaskAsync(queue: string, taskId: string) { }; } +export function cancelAllActiveTasksAsync(queue: string) { + return async (dispatch: Dispatch) => { + dispatch({ type: CANCEL_ALL_ACTIVE_TASKS_BEGIN, queue }); + try { + await cancelAllActiveTasks(queue); + dispatch({ type: CANCEL_ALL_ACTIVE_TASKS_SUCCESS, queue }); + } catch (error) { + console.error("cancelAllActiveTasksAsync: ", error); + dispatch({ + type: CANCEL_ALL_ACTIVE_TASKS_ERROR, + error: "Could not cancel all tasks", + queue, + }); + } + }; +} + +export function batchCancelActiveTasksAsync(queue: string, taskIds: string[]) { + return async (dispatch: Dispatch) => { + dispatch({ type: BATCH_CANCEL_ACTIVE_TASKS_BEGIN, queue, taskIds }); + try { + const response = await batchCancelActiveTasks(queue, taskIds); + dispatch({ + type: BATCH_CANCEL_ACTIVE_TASKS_SUCCESS, + queue: queue, + payload: response, + }); + } catch (error) { + console.error("batchCancelActiveTasksAsync: ", error); + dispatch({ + type: BATCH_CANCEL_ACTIVE_TASKS_ERROR, + error: `Could not batch cancel tasks: ${taskIds}`, + queue, + taskIds, + }); + } + }; +} + export function runScheduledTaskAsync(queue: string, taskKey: string) { return async (dispatch: Dispatch) => { dispatch({ type: RUN_SCHEDULED_TASK_BEGIN, queue, taskKey }); diff --git a/ui/src/api.ts b/ui/src/api.ts index f23cd3f..a57cb25 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -41,6 +41,11 @@ export interface ListSchedulerEntriesResponse { entries: SchedulerEntry[]; } +export interface BatchCancelTasksResponse { + canceled_ids: string[]; + error_ids: string[]; +} + export interface BatchDeleteTasksResponse { deleted_keys: string[]; failed_keys: string[]; @@ -198,6 +203,27 @@ export async function cancelActiveTask( }); } +export async function cancelAllActiveTasks(qname: string): Promise { + await axios({ + method: "post", + url: `${BASE_URL}/queues/${qname}/active_tasks:cancel_all`, + }); +} + +export async function batchCancelActiveTasks( + qname: string, + taskIds: string[] +): Promise { + const resp = await axios({ + method: "post", + url: `${BASE_URL}/queues/${qname}/active_tasks:batch_cancel`, + data: { + task_ids: taskIds, + }, + }); + return resp.data; +} + export async function listPendingTasks( qname: string, pageOpts?: PaginationOptions diff --git a/ui/src/components/ActiveTasksTable.tsx b/ui/src/components/ActiveTasksTable.tsx index c52baea..22c105d 100644 --- a/ui/src/components/ActiveTasksTable.tsx +++ b/ui/src/components/ActiveTasksTable.tsx @@ -26,6 +26,8 @@ import syntaxHighlightStyle from "react-syntax-highlighter/dist/esm/styles/hljs/ import { listActiveTasksAsync, cancelActiveTaskAsync, + batchCancelActiveTasksAsync, + cancelAllActiveTasksAsync, } from "../actions/tasksActions"; import { AppState } from "../store"; import TablePaginationActions, { @@ -47,11 +49,18 @@ function mapStateToProps(state: AppState) { return { loading: state.tasks.activeTasks.loading, tasks: state.tasks.activeTasks.data, + batchActionPending: state.tasks.activeTasks.batchActionPending, + allActionPending: state.tasks.activeTasks.allActionPending, pollInterval: state.settings.pollInterval, }; } -const mapDispatchToProps = { listActiveTasksAsync, cancelActiveTaskAsync }; +const mapDispatchToProps = { + listActiveTasksAsync, + cancelActiveTaskAsync, + batchCancelActiveTasksAsync, + cancelAllActiveTasksAsync, +}; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -91,6 +100,16 @@ function ActiveTasksTable(props: Props & ReduxProps) { } }; + const handleCancelAllClick = () => { + props.cancelAllActiveTasksAsync(queue); + }; + + const handleBatchCancelClick = () => { + props + .batchCancelActiveTasksAsync(queue, selectedIds) + .then(() => setSelectedIds([])); + }; + const fetchData = useCallback(() => { const pageOpts = { page: page + 1, size: pageSize }; listActiveTasksAsync(queue, pageOpts); @@ -124,15 +143,15 @@ function ActiveTasksTable(props: Props & ReduxProps) { { tooltip: "Cancel", icon: , - onClick: () => console.log("TODO"), - disabled: false, + onClick: handleBatchCancelClick, + disabled: props.batchActionPending, }, ]} menuItemActions={[ { label: "Cancel All", - onClick: () => console.log("TODO"), - disabled: false, + onClick: handleCancelAllClick, + disabled: props.allActionPending, }, ]} /> diff --git a/ui/src/components/TableActions.tsx b/ui/src/components/TableActions.tsx index 522b570..733b778 100644 --- a/ui/src/components/TableActions.tsx +++ b/ui/src/components/TableActions.tsx @@ -70,7 +70,10 @@ export default function TableActions(props: Props) { {props.menuItemActions.map((action) => ( { + action.onClick(); + closeMenu(); + }} disabled={action.disabled} > {action.label} diff --git a/ui/src/reducers/snackbarReducer.ts b/ui/src/reducers/snackbarReducer.ts index 623caf4..cfe1c7c 100644 --- a/ui/src/reducers/snackbarReducer.ts +++ b/ui/src/reducers/snackbarReducer.ts @@ -3,6 +3,7 @@ import { SnackbarActionTypes, } from "../actions/snackbarActions"; import { + BATCH_CANCEL_ACTIVE_TASKS_SUCCESS, BATCH_DELETE_DEAD_TASKS_SUCCESS, BATCH_DELETE_RETRY_TASKS_SUCCESS, BATCH_DELETE_SCHEDULED_TASKS_SUCCESS, @@ -11,6 +12,7 @@ import { BATCH_RUN_DEAD_TASKS_SUCCESS, BATCH_RUN_RETRY_TASKS_SUCCESS, BATCH_RUN_SCHEDULED_TASKS_SUCCESS, + CANCEL_ALL_ACTIVE_TASKS_SUCCESS, DELETE_ALL_DEAD_TASKS_SUCCESS, DELETE_ALL_RETRY_TASKS_SUCCESS, DELETE_ALL_SCHEDULED_TASKS_SUCCESS, @@ -53,6 +55,22 @@ function snackbarReducer( isOpen: false, }; + case BATCH_CANCEL_ACTIVE_TASKS_SUCCESS: { + const n = action.payload.canceled_ids.length; + return { + isOpen: true, + message: `Cancelation signal sent to ${n} ${ + n === 1 ? "task" : "tasks" + }`, + }; + } + + case CANCEL_ALL_ACTIVE_TASKS_SUCCESS: + return { + isOpen: true, + message: `Cancelation signal sent to all tasks in ${action.queue} queue`, + }; + case RUN_SCHEDULED_TASK_SUCCESS: return { isOpen: true, diff --git a/ui/src/reducers/tasksReducer.ts b/ui/src/reducers/tasksReducer.ts index 5a811ac..cd547d3 100644 --- a/ui/src/reducers/tasksReducer.ts +++ b/ui/src/reducers/tasksReducer.ts @@ -90,6 +90,12 @@ import { BATCH_KILL_RETRY_TASKS_SUCCESS, BATCH_KILL_RETRY_TASKS_BEGIN, BATCH_KILL_RETRY_TASKS_ERROR, + BATCH_CANCEL_ACTIVE_TASKS_BEGIN, + BATCH_CANCEL_ACTIVE_TASKS_SUCCESS, + BATCH_CANCEL_ACTIVE_TASKS_ERROR, + CANCEL_ALL_ACTIVE_TASKS_BEGIN, + CANCEL_ALL_ACTIVE_TASKS_SUCCESS, + CANCEL_ALL_ACTIVE_TASKS_ERROR, } from "../actions/tasksActions"; import { ActiveTask, @@ -130,6 +136,8 @@ export interface DeadTaskExtended extends DeadTask { interface TasksState { activeTasks: { loading: boolean; + batchActionPending: boolean; + allActionPending: boolean; error: string; data: ActiveTaskExtended[]; }; @@ -164,6 +172,8 @@ interface TasksState { const initialState: TasksState = { activeTasks: { loading: false, + batchActionPending: false, + allActionPending: false, error: "", data: [], }, @@ -214,6 +224,7 @@ function tasksReducer( return { ...state, activeTasks: { + ...state.activeTasks, loading: false, error: "", data: action.payload.tasks.map((task) => ({ @@ -413,6 +424,103 @@ function tasksReducer( }, }; + case BATCH_CANCEL_ACTIVE_TASKS_BEGIN: { + const newData = state.activeTasks.data.map((task) => { + if (!action.taskIds.includes(task.id)) { + return task; + } + return { ...task, requestPending: true }; + }); + return { + ...state, + activeTasks: { + ...state.activeTasks, + batchActionPending: true, + data: newData, + }, + }; + } + + case BATCH_CANCEL_ACTIVE_TASKS_SUCCESS: { + const newData = state.activeTasks.data.map((task) => { + if (action.payload.canceled_ids.includes(task.id)) { + return { ...task, canceling: true, requestPending: false }; + } + if (action.payload.error_ids.includes(task.id)) { + return { ...task, requestPending: false }; + } + return task; + }); + return { + ...state, + activeTasks: { + ...state.activeTasks, + batchActionPending: false, + data: newData, + }, + }; + } + + case BATCH_CANCEL_ACTIVE_TASKS_ERROR: { + const newData = state.activeTasks.data.map((task) => { + return { ...task, requestPending: false }; + }); + return { + ...state, + activeTasks: { + ...state.activeTasks, + batchActionPending: false, + data: newData, + }, + }; + } + + case CANCEL_ALL_ACTIVE_TASKS_BEGIN: { + const newData = state.activeTasks.data.map((task) => ({ + ...task, + requestPending: true, + })); + return { + ...state, + activeTasks: { + ...state.activeTasks, + allActionPending: true, + data: newData, + }, + }; + } + + case CANCEL_ALL_ACTIVE_TASKS_SUCCESS: { + const newData = state.activeTasks.data.map((task) => ({ + ...task, + requestPending: false, + canceling: true, + })); + return { + ...state, + activeTasks: { + ...state.activeTasks, + allActionPending: false, + data: newData, + }, + }; + } + + case CANCEL_ALL_ACTIVE_TASKS_ERROR: { + const newData = state.activeTasks.data.map((task) => ({ + ...task, + requestPending: false, + })); + return { + ...state, + activeTasks: { + ...state.activeTasks, + allActionPending: false, + data: newData, + }, + }; + } + case RUN_SCHEDULED_TASK_BEGIN: case KILL_SCHEDULED_TASK_BEGIN: case DELETE_SCHEDULED_TASK_BEGIN: