From 4110955aab5eb5313daf59bb1f6ebedb59856b82 Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Mon, 18 Oct 2021 07:40:48 -0700 Subject: [PATCH] (ui): Add actions and reducer for deleting completed tasks --- ui/src/actions/tasksActions.ts | 183 ++++++++++++++++++++-- ui/src/api.ts | 62 ++++++-- ui/src/components/CompletedTasksTable.tsx | 30 +++- ui/src/reducers/queuesReducer.ts | 60 ++++++- ui/src/reducers/snackbarReducer.ts | 22 +++ ui/src/reducers/tasksReducer.ts | 148 +++++++++++++++-- 6 files changed, 460 insertions(+), 45 deletions(-) diff --git a/ui/src/actions/tasksActions.ts b/ui/src/actions/tasksActions.ts index 5ff2a6d..7402d6e 100644 --- a/ui/src/actions/tasksActions.ts +++ b/ui/src/actions/tasksActions.ts @@ -4,6 +4,7 @@ import { batchDeleteArchivedTasks, batchDeleteRetryTasks, batchDeleteScheduledTasks, + batchDeleteCompletedTasks, BatchDeleteTasksResponse, batchArchiveRetryTasks, batchArchiveScheduledTasks, @@ -17,9 +18,11 @@ import { deleteAllArchivedTasks, deleteAllRetryTasks, deleteAllScheduledTasks, + deleteAllCompletedTasks, deleteArchivedTask, deleteRetryTask, deleteScheduledTask, + deleteCompletedTask, archiveAllRetryTasks, archiveAllScheduledTasks, archiveRetryTask, @@ -218,6 +221,21 @@ export const DELETE_ALL_ARCHIVED_TASKS_SUCCESS = "DELETE_ALL_ARCHIVED_TASKS_SUCCESS"; export const DELETE_ALL_ARCHIVED_TASKS_ERROR = "DELETE_ALL_ARCHIVED_TASKS_ERROR"; +export const DELETE_COMPLETED_TASK_BEGIN = "DELETE_COMPLETED_TASK_BEGIN"; +export const DELETE_COMPLETED_TASK_SUCCESS = "DELETE_COMPLETED_TASK_SUCCESS"; +export const DELETE_COMPLETED_TASK_ERROR = "DELETE_COMPLETED_TASK_ERROR"; +export const DELETE_ALL_COMPLETED_TASKS_BEGIN = + "DELETE_ALL_COMPLETED_TASKS_BEGIN"; +export const DELETE_ALL_COMPLETED_TASKS_SUCCESS = + "DELETE_ALL_COMPLETED_TASKS_SUCCESS"; +export const DELETE_ALL_COMPLETED_TASKS_ERROR = + "DELETE_ALL_COMPLETED_TASKS_ERROR"; +export const BATCH_DELETE_COMPLETED_TASKS_BEGIN = + "BATCH_DELETE_COMPLETED_TASKS_BEGIN"; +export const BATCH_DELETE_COMPLETED_TASKS_SUCCESS = + "BATCH_DELETE_COMPLETED_TASKS_SUCCESS"; +export const BATCH_DELETE_COMPLETED_TASKS_ERROR = + "BATCH_DELETE_COMPLETED_TASKS_ERROR"; interface GetTaskInfoBeginAction { type: typeof GET_TASK_INFO_BEGIN; @@ -933,6 +951,61 @@ interface DeleteAllArchivedTasksErrorAction { error: string; } +interface DeleteCompletedTaskBeginAction { + type: typeof DELETE_COMPLETED_TASK_BEGIN; + queue: string; + taskId: string; +} + +interface DeleteCompletedTaskSuccessAction { + type: typeof DELETE_COMPLETED_TASK_SUCCESS; + queue: string; + taskId: string; +} + +interface DeleteCompletedTaskErrorAction { + type: typeof DELETE_COMPLETED_TASK_ERROR; + queue: string; + taskId: string; + error: string; +} + +interface BatchDeleteCompletedTasksBeginAction { + type: typeof BATCH_DELETE_COMPLETED_TASKS_BEGIN; + queue: string; + taskIds: string[]; +} + +interface BatchDeleteCompletedTasksSuccessAction { + type: typeof BATCH_DELETE_COMPLETED_TASKS_SUCCESS; + queue: string; + payload: BatchDeleteTasksResponse; +} + +interface BatchDeleteCompletedTasksErrorAction { + type: typeof BATCH_DELETE_COMPLETED_TASKS_ERROR; + queue: string; + taskIds: string[]; + error: string; +} + +interface DeleteAllCompletedTasksBeginAction { + type: typeof DELETE_ALL_COMPLETED_TASKS_BEGIN; + queue: string; +} + +interface DeleteAllCompletedTasksSuccessAction { + type: typeof DELETE_ALL_COMPLETED_TASKS_SUCCESS; + queue: string; + deleted: number; +} + +interface DeleteAllCompletedTasksErrorAction { + type: typeof DELETE_ALL_COMPLETED_TASKS_ERROR; + queue: string; + error: string; +} + // Union of all tasks related action types. export type TasksActionTypes = | GetTaskInfoBeginAction @@ -1054,7 +1127,16 @@ export type TasksActionTypes = | RunAllArchivedTasksErrorAction | DeleteAllArchivedTasksBeginAction | DeleteAllArchivedTasksSuccessAction - | DeleteAllArchivedTasksErrorAction; + | DeleteAllArchivedTasksErrorAction + | DeleteCompletedTaskBeginAction + | DeleteCompletedTaskSuccessAction + | DeleteCompletedTaskErrorAction + | BatchDeleteCompletedTasksBeginAction + | BatchDeleteCompletedTasksSuccessAction + | BatchDeleteCompletedTasksErrorAction + | DeleteAllCompletedTasksBeginAction + | DeleteAllCompletedTasksSuccessAction + | DeleteAllCompletedTasksErrorAction; export function getTaskInfoAsync(qname: string, id: string) { return async (dispatch: Dispatch) => { @@ -1064,15 +1146,15 @@ export function getTaskInfoAsync(qname: string, id: string) { dispatch({ type: GET_TASK_INFO_SUCCESS, payload: response, - }) + }); } catch (error) { console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error)); dispatch({ type: GET_TASK_INFO_ERROR, error: toErrorString(error), - }) + }); } - } + }; } export function listActiveTasksAsync( @@ -1210,16 +1292,19 @@ export function listArchivedTasksAsync( }; } -export function listCompletedTasksAsync(qname: string, pageOpts?: PaginationOptions) { +export function listCompletedTasksAsync( + qname: string, + pageOpts?: PaginationOptions +) { return async (dispatch: Dispatch) => { try { - dispatch({ type: LIST_COMPLETED_TASKS_BEGIN, queue: qname }) + dispatch({ type: LIST_COMPLETED_TASKS_BEGIN, queue: qname }); const response = await listCompletedTasks(qname, pageOpts); dispatch({ type: LIST_COMPLETED_TASKS_SUCCESS, queue: qname, payload: response, - }) + }); } catch (error) { console.error( "listCompletedTasksAsync: ", @@ -1228,10 +1313,10 @@ export function listCompletedTasksAsync(qname: string, pageOpts?: PaginationOpti dispatch({ type: LIST_COMPLETED_TASKS_ERROR, queue: qname, - error: toErrorString(error) - }) + error: toErrorString(error), + }); } - } + }; } export function cancelActiveTaskAsync(queue: string, taskId: string) { @@ -1444,10 +1529,7 @@ export function deletePendingTaskAsync(queue: string, taskId: string) { }; } -export function batchDeletePendingTasksAsync( - queue: string, - taskIds: string[] -) { +export function batchDeletePendingTasksAsync(queue: string, taskIds: string[]) { return async (dispatch: Dispatch) => { dispatch({ type: BATCH_DELETE_PENDING_TASKS_BEGIN, queue, taskIds }); try { @@ -1987,3 +2069,76 @@ export function runAllArchivedTasksAsync(queue: string) { } }; } + +export function deleteCompletedTaskAsync(queue: string, taskId: string) { + return async (dispatch: Dispatch) => { + dispatch({ type: DELETE_COMPLETED_TASK_BEGIN, queue, taskId }); + try { + await deleteCompletedTask(queue, taskId); + dispatch({ type: DELETE_COMPLETED_TASK_SUCCESS, queue, taskId }); + } catch (error) { + console.error( + "deleteCompletedTaskAsync: ", + toErrorStringWithHttpStatus(error) + ); + dispatch({ + type: DELETE_COMPLETED_TASK_ERROR, + error: toErrorString(error), + queue, + taskId, + }); + } + }; +} + +export function batchDeleteCompletedTasksAsync( + queue: string, + taskIds: string[] +) { + return async (dispatch: Dispatch) => { + dispatch({ type: BATCH_DELETE_COMPLETED_TASKS_BEGIN, queue, taskIds }); + try { + const response = await batchDeleteCompletedTasks(queue, taskIds); + dispatch({ + type: BATCH_DELETE_COMPLETED_TASKS_SUCCESS, + queue: queue, + payload: response, + }); + } catch (error) { + console.error( + "batchDeleteCompletedTasksAsync: ", + toErrorStringWithHttpStatus(error) + ); + dispatch({ + type: BATCH_DELETE_COMPLETED_TASKS_ERROR, + error: toErrorString(error), + queue, + taskIds, + }); + } + }; +} + +export function deleteAllCompletedTasksAsync(queue: string) { + return async (dispatch: Dispatch) => { + dispatch({ type: DELETE_ALL_COMPLETED_TASKS_BEGIN, queue }); + try { + const response = await deleteAllCompletedTasks(queue); + dispatch({ + type: DELETE_ALL_COMPLETED_TASKS_SUCCESS, + deleted: response.deleted, + queue, + }); + } catch (error) { + console.error( + "deleteAllCompletedTasksAsync: ", + toErrorStringWithHttpStatus(error) + ); + dispatch({ + type: DELETE_ALL_COMPLETED_TASKS_ERROR, + error: toErrorString(error), + queue, + }); + } + }; +} diff --git a/ui/src/api.ts b/ui/src/api.ts index 5aab5fa..a2421f6 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -5,7 +5,9 @@ import queryString from "query-string"; // the static file server. // In developement, we assume that the API server is listening on port 8080. const BASE_URL = - process.env.NODE_ENV === "production" ? `${window.ROOT_PATH}/api` : `http://localhost:8080${window.ROOT_PATH}/api`; + process.env.NODE_ENV === "production" + ? `${window.ROOT_PATH}/api` + : `http://localhost:8080${window.ROOT_PATH}/api`; export interface ListQueuesResponse { queues: Queue[]; @@ -244,7 +246,7 @@ export interface Queue { scheduled: number; retry: number; archived: number; - completed: number, + completed: number; processed: number; failed: number; timestamp: string; @@ -333,7 +335,7 @@ export interface CompletedTask extends BaseTask { retried: number; completed_at: string; result: string; - ttl_seconds: number + ttl_seconds: number; } export interface ServerInfo { @@ -415,7 +417,10 @@ export async function listQueueStats(): Promise { return resp.data; } -export async function getTaskInfo(qname: string, id: string): Promise { +export async function getTaskInfo( + qname: string, + id: string +): Promise { const url = `${BASE_URL}/queues/${qname}/tasks/${id}`; const resp = await axios({ method: "get", @@ -530,16 +535,19 @@ export async function listArchivedTasks( return resp.data; } -export async function listCompletedTasks(qname: string, pageOpts?: PaginationOptions): Promise { - let url = `${BASE_URL}/queues/${qname}/completed_tasks` +export async function listCompletedTasks( + qname: string, + pageOpts?: PaginationOptions +): Promise { + let url = `${BASE_URL}/queues/${qname}/completed_tasks`; if (pageOpts) { - url += `?${queryString.stringify(pageOpts)}` + url += `?${queryString.stringify(pageOpts)}`; } const resp = await axios({ - method: "get", + method: "get", url, - }) - return resp.data + }); + return resp.data; } export async function archivePendingTask( @@ -864,6 +872,40 @@ export async function runAllArchivedTasks(qname: string): Promise { }); } +export async function deleteCompletedTask( + qname: string, + taskId: string +): Promise { + await axios({ + method: "delete", + url: `${BASE_URL}/queues/${qname}/completed_tasks/${taskId}`, + }); +} + +export async function batchDeleteCompletedTasks( + qname: string, + taskIds: string[] +): Promise { + const resp = await axios({ + method: "post", + url: `${BASE_URL}/queues/${qname}/completed_tasks:batch_delete`, + data: { + task_ids: taskIds, + }, + }); + return resp.data; +} + +export async function deleteAllCompletedTasks( + qname: string +): Promise { + const resp = await axios({ + method: "delete", + url: `${BASE_URL}/queues/${qname}/completed_tasks:delete_all`, + }); + return resp.data; +} + export async function listServers(): Promise { const resp = await axios({ method: "get", diff --git a/ui/src/components/CompletedTasksTable.tsx b/ui/src/components/CompletedTasksTable.tsx index d9fb37e..d78ee32 100644 --- a/ui/src/components/CompletedTasksTable.tsx +++ b/ui/src/components/CompletedTasksTable.tsx @@ -20,7 +20,12 @@ import Alert from "@material-ui/lab/Alert"; import AlertTitle from "@material-ui/lab/AlertTitle"; import SyntaxHighlighter from "./SyntaxHighlighter"; import { AppState } from "../store"; -import { listCompletedTasksAsync } from "../actions/tasksActions"; +import { + listCompletedTasksAsync, + deleteAllCompletedTasksAsync, + deleteCompletedTaskAsync, + batchDeleteCompletedTasksAsync, +} from "../actions/tasksActions"; import TablePaginationActions, { rowsPerPageOptions, } from "./TablePaginationActions"; @@ -67,6 +72,9 @@ function mapStateToProps(state: AppState) { const mapDispatchToProps = { listCompletedTasksAsync, + deleteCompletedTaskAsync, + deleteAllCompletedTasksAsync, + batchDeleteCompletedTasksAsync, taskRowsPerPageChange, }; @@ -109,6 +117,16 @@ function CompletedTasksTable(props: Props & ReduxProps) { } }; + const handleDeleteAllClick = () => { + props.deleteAllCompletedTasksAsync(queue); + }; + + const handleBatchDeleteClick = () => { + props + .batchDeleteCompletedTasksAsync(queue, selectedIds) + .then(() => setSelectedIds([])); + }; + const fetchData = useCallback(() => { const pageOpts = { page: page + 1, size: pageSize }; listCompletedTasksAsync(queue, pageOpts); @@ -153,18 +171,14 @@ function CompletedTasksTable(props: Props & ReduxProps) { { tooltip: "Delete", icon: , - onClick: () => { - /* TODO */ - }, + onClick: handleBatchDeleteClick, disabled: props.batchActionPending, }, ]} menuItemActions={[ { label: "Delete All", - onClick: () => { - /* TODO */ - }, + onClick: handleDeleteAllClick, disabled: props.allActionPending, }, ]} @@ -218,7 +232,7 @@ function CompletedTasksTable(props: Props & ReduxProps) { } }} onDeleteClick={() => { - // props.deleteCompletedTaskAsync(queue, task.id); + props.deleteCompletedTaskAsync(queue, task.id); }} allActionPending={props.allActionPending} onActionCellEnter={() => setActiveTaskId(task.id)} diff --git a/ui/src/reducers/queuesReducer.ts b/ui/src/reducers/queuesReducer.ts index 64ad33d..ea1dcf7 100644 --- a/ui/src/reducers/queuesReducer.ts +++ b/ui/src/reducers/queuesReducer.ts @@ -50,6 +50,9 @@ import { BATCH_DELETE_PENDING_TASKS_SUCCESS, ARCHIVE_ALL_PENDING_TASKS_SUCCESS, DELETE_ALL_PENDING_TASKS_SUCCESS, + DELETE_COMPLETED_TASK_SUCCESS, + DELETE_ALL_COMPLETED_TASKS_SUCCESS, + BATCH_DELETE_COMPLETED_TASKS_SUCCESS, } from "../actions/tasksActions"; import { Queue } from "../api"; @@ -550,8 +553,7 @@ function queuesReducer( queueInfo.currentStats.pending + action.payload.archived_ids.length, retry: - queueInfo.currentStats.retry - - action.payload.archived_ids.length, + queueInfo.currentStats.retry - action.payload.archived_ids.length, }, }; }); @@ -647,6 +649,23 @@ function queuesReducer( return { ...state, data: newData }; } + case DELETE_COMPLETED_TASK_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + size: queueInfo.currentStats.size - 1, + completed: queueInfo.currentStats.completed - 1, + }, + }; + }); + return { ...state, data: newData }; + } + case BATCH_RUN_ARCHIVED_TASKS_SUCCESS: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { @@ -688,6 +707,26 @@ function queuesReducer( return { ...state, data: newData }; } + case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + size: + queueInfo.currentStats.size - action.payload.deleted_ids.length, + completed: + queueInfo.currentStats.completed - + action.payload.deleted_ids.length, + }, + }; + }); + return { ...state, data: newData }; + } + case RUN_ALL_ARCHIVED_TASKS_SUCCESS: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { @@ -723,6 +762,23 @@ function queuesReducer( return { ...state, data: newData }; } + case DELETE_ALL_COMPLETED_TASKS_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + size: queueInfo.currentStats.size - action.deleted, + completed: 0, + }, + }; + }); + return { ...state, data: newData }; + } + default: return state; } diff --git a/ui/src/reducers/snackbarReducer.ts b/ui/src/reducers/snackbarReducer.ts index aaf8574..a09ef83 100644 --- a/ui/src/reducers/snackbarReducer.ts +++ b/ui/src/reducers/snackbarReducer.ts @@ -36,6 +36,9 @@ import { BATCH_DELETE_PENDING_TASKS_SUCCESS, ARCHIVE_ALL_PENDING_TASKS_SUCCESS, DELETE_ALL_PENDING_TASKS_SUCCESS, + DELETE_COMPLETED_TASK_SUCCESS, + DELETE_ALL_COMPLETED_TASKS_SUCCESS, + BATCH_DELETE_COMPLETED_TASKS_SUCCESS, } from "../actions/tasksActions"; interface SnackbarState { @@ -285,6 +288,25 @@ function snackbarReducer( message: "All archived tasks deleted", }; + case DELETE_COMPLETED_TASK_SUCCESS: + return { + isOpen: true, + message: `Completed task deleted`, + }; + + case DELETE_ALL_COMPLETED_TASKS_SUCCESS: + return { + isOpen: true, + message: "All completed tasks deleted", + }; + + case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: + const n = action.payload.deleted_ids.length; + return { + isOpen: true, + message: `${n} completed ${n === 1 ? "task" : "tasks"} deleted`, + }; + default: return state; } diff --git a/ui/src/reducers/tasksReducer.ts b/ui/src/reducers/tasksReducer.ts index e8b09ae..3bda9fa 100644 --- a/ui/src/reducers/tasksReducer.ts +++ b/ui/src/reducers/tasksReducer.ts @@ -120,6 +120,15 @@ import { GET_TASK_INFO_BEGIN, GET_TASK_INFO_ERROR, GET_TASK_INFO_SUCCESS, + DELETE_COMPLETED_TASK_BEGIN, + DELETE_COMPLETED_TASK_ERROR, + DELETE_COMPLETED_TASK_SUCCESS, + DELETE_ALL_COMPLETED_TASKS_BEGIN, + DELETE_ALL_COMPLETED_TASKS_ERROR, + DELETE_ALL_COMPLETED_TASKS_SUCCESS, + BATCH_DELETE_COMPLETED_TASKS_BEGIN, + BATCH_DELETE_COMPLETED_TASKS_ERROR, + BATCH_DELETE_COMPLETED_TASKS_SUCCESS, } from "../actions/tasksActions"; import { ActiveTask, @@ -213,12 +222,12 @@ interface TasksState { allActionPending: boolean; error: string; data: CompletedTaskExtended[]; - } + }; taskInfo: { loading: boolean; error: string; data?: TaskInfo; - }, + }; } const initialState: TasksState = { @@ -267,7 +276,7 @@ const initialState: TasksState = { taskInfo: { loading: false, error: "", - } + }, }; function tasksReducer( @@ -282,16 +291,16 @@ function tasksReducer( ...state.taskInfo, loading: true, }, - } + }; case GET_TASK_INFO_ERROR: - return { - ...state, - taskInfo: { - loading: false, - error: action.error, - }, - }; + return { + ...state, + taskInfo: { + loading: false, + error: action.error, + }, + }; case GET_TASK_INFO_SUCCESS: return { @@ -508,6 +517,123 @@ function tasksReducer( }, }; + case DELETE_COMPLETED_TASK_BEGIN: + return { + ...state, + completedTasks: { + ...state.completedTasks, + data: state.completedTasks.data.map((task) => { + if (task.id !== action.taskId) { + return task; + } + return { ...task, requestPending: true }; + }), + }, + }; + + case DELETE_COMPLETED_TASK_SUCCESS: + return { + ...state, + completedTasks: { + ...state.completedTasks, + data: state.completedTasks.data.filter( + (task) => task.id !== action.taskId + ), + }, + }; + + case DELETE_COMPLETED_TASK_ERROR: + return { + ...state, + completedTasks: { + ...state.completedTasks, + data: state.completedTasks.data.map((task) => { + if (task.id !== action.taskId) { + return task; + } + return { ...task, requestPending: false }; + }), + }, + }; + + case DELETE_ALL_COMPLETED_TASKS_BEGIN: + return { + ...state, + completedTasks: { + ...state.completedTasks, + allActionPending: true, + }, + }; + + case DELETE_ALL_COMPLETED_TASKS_SUCCESS: + return { + ...state, + completedTasks: { + ...state.completedTasks, + allActionPending: false, + data: [], + }, + }; + + case DELETE_ALL_COMPLETED_TASKS_ERROR: + return { + ...state, + completedTasks: { + ...state.completedTasks, + allActionPending: false, + }, + }; + + case BATCH_DELETE_COMPLETED_TASKS_BEGIN: + return { + ...state, + completedTasks: { + ...state.completedTasks, + batchActionPending: true, + data: state.completedTasks.data.map((task) => { + if (!action.taskIds.includes(task.id)) { + return task; + } + return { + ...task, + requestPending: true, + }; + }), + }, + }; + + case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: { + const newData = state.completedTasks.data.filter( + (task) => !action.payload.deleted_ids.includes(task.id) + ); + return { + ...state, + completedTasks: { + ...state.completedTasks, + batchActionPending: false, + data: newData, + }, + }; + } + + case BATCH_DELETE_COMPLETED_TASKS_ERROR: + return { + ...state, + completedTasks: { + ...state.completedTasks, + batchActionPending: false, + data: state.completedTasks.data.map((task) => { + if (!action.taskIds.includes(task.id)) { + return task; + } + return { + ...task, + requestPending: false, + }; + }), + }, + }; + case CANCEL_ACTIVE_TASK_BEGIN: { const newData = state.activeTasks.data.map((task) => { if (task.id !== action.taskId) {