From 601a7c8addb0a5055964adcf49f2fc319e59fd0b Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Tue, 8 Dec 2020 21:22:23 -0800 Subject: [PATCH] Add delete task button to RetryTasksTable --- conversion_helpers.go | 6 +++ ui/src/actions/tasksActions.ts | 45 +++++++++++++++++++++- ui/src/api.ts | 13 +++++++ ui/src/components/RetryTasksTable.tsx | 23 ++++++++--- ui/src/reducers/queuesReducer.ts | 17 +++++++++ ui/src/reducers/tasksReducer.ts | 55 ++++++++++++++++++++++++++- 6 files changed, 150 insertions(+), 9 deletions(-) diff --git a/conversion_helpers.go b/conversion_helpers.go index 93381f5..21ef339 100644 --- a/conversion_helpers.go +++ b/conversion_helpers.go @@ -125,6 +125,7 @@ func toPendingTasks(in []*asynq.PendingTask) []*PendingTask { type ScheduledTask struct { *BaseTask + Key string `json:"key"` NextProcessAt time.Time `json:"next_process_at"` } @@ -137,6 +138,7 @@ func toScheduledTask(t *asynq.ScheduledTask) *ScheduledTask { } return &ScheduledTask{ BaseTask: base, + Key: t.Key(), NextProcessAt: t.NextProcessAt, } } @@ -151,6 +153,7 @@ func toScheduledTasks(in []*asynq.ScheduledTask) []*ScheduledTask { type RetryTask struct { *BaseTask + Key string `json:"key"` NextProcessAt time.Time `json:"next_process_at"` MaxRetry int `json:"max_retry"` Retried int `json:"retried"` @@ -166,6 +169,7 @@ func toRetryTask(t *asynq.RetryTask) *RetryTask { } return &RetryTask{ BaseTask: base, + Key: t.Key(), NextProcessAt: t.NextProcessAt, MaxRetry: t.MaxRetry, Retried: t.Retried, @@ -183,6 +187,7 @@ func toRetryTasks(in []*asynq.RetryTask) []*RetryTask { type DeadTask struct { *BaseTask + Key string `json:"key"` MaxRetry int `json:"max_retry"` Retried int `json:"retried"` ErrorMsg string `json:"error_message"` @@ -198,6 +203,7 @@ func toDeadTask(t *asynq.DeadTask) *DeadTask { } return &DeadTask{ BaseTask: base, + Key: t.Key(), MaxRetry: t.MaxRetry, Retried: t.Retried, ErrorMsg: t.ErrorMsg, diff --git a/ui/src/actions/tasksActions.ts b/ui/src/actions/tasksActions.ts index 57caf27..c3e30fe 100644 --- a/ui/src/actions/tasksActions.ts +++ b/ui/src/actions/tasksActions.ts @@ -1,5 +1,6 @@ import { cancelActiveTask, + deleteRetryTask, listActiveTasks, ListActiveTasksResponse, listDeadTasks, @@ -33,6 +34,9 @@ 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 DELETE_RETRY_TASK_BEGIN = "DELETE_RETRY_TASK_BEGIN"; +export const DELETE_RETRY_TASK_SUCCESS = "DELETE_RETRY_TASK_SUCCESS"; +export const DELETE_RETRY_TASK_ERROR = "DELETE_RETRY_TASK_ERROR"; interface ListActiveTasksBeginAction { type: typeof LIST_ACTIVE_TASKS_BEGIN; @@ -138,6 +142,24 @@ interface CancelActiveTaskErrorAction { error: string; } +interface DeleteRetryTaskBeginAction { + type: typeof DELETE_RETRY_TASK_BEGIN; + queue: string; + taskKey: string; +} + +interface DeleteRetryTaskSuccessAction { + type: typeof DELETE_RETRY_TASK_SUCCESS; + queue: string; + taskKey: string; +} + +interface DeleteRetryTaskErrorAction { + type: typeof DELETE_RETRY_TASK_ERROR; + queue: string; + taskKey: string; + error: string; +} // Union of all tasks related action types. export type TasksActionTypes = | ListActiveTasksBeginAction @@ -157,7 +179,10 @@ export type TasksActionTypes = | ListDeadTasksErrorAction | CancelActiveTaskBeginAction | CancelActiveTaskSuccessAction - | CancelActiveTaskErrorAction; + | CancelActiveTaskErrorAction + | DeleteRetryTaskBeginAction + | DeleteRetryTaskSuccessAction + | DeleteRetryTaskErrorAction; export function listActiveTasksAsync( qname: string, @@ -290,3 +315,21 @@ export function cancelActiveTaskAsync(queue: string, taskId: string) { } }; } + +export function deleteRetryTaskAsync(queue: string, taskKey: string) { + return async (dispatch: Dispatch) => { + dispatch({ type: DELETE_RETRY_TASK_BEGIN, queue, taskKey }); + try { + await deleteRetryTask(queue, taskKey); + dispatch({ type: DELETE_RETRY_TASK_SUCCESS, queue, taskKey }); + } catch (error) { + console.error("deleteRetryTaskAsync: ", error); + dispatch({ + type: DELETE_RETRY_TASK_ERROR, + error: `Could not delete task: ${taskKey}`, + queue, + taskKey, + }); + } + }; +} diff --git a/ui/src/api.ts b/ui/src/api.ts index e91a52a..d51f2d6 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -79,12 +79,14 @@ export interface PendingTask extends BaseTask { export interface ScheduledTask extends BaseTask { id: string; + key: string; queue: string; next_process_at: string; } export interface RetryTask extends BaseTask { id: string; + key: string; queue: string; next_process_at: string; max_retry: number; @@ -94,6 +96,7 @@ export interface RetryTask extends BaseTask { export interface DeadTask extends BaseTask { id: string; + key: string; queue: string; max_retry: number; retried: number; @@ -240,6 +243,16 @@ export async function listDeadTasks( return resp.data; } +export async function deleteRetryTask( + qname: string, + taskKey: string +): Promise { + await axios({ + method: "delete", + url: `${BASE_URL}/queues/${qname}/retry_tasks/${taskKey}`, + }); +} + export async function listSchedulerEntries(): Promise { const resp = await axios({ method: "get", diff --git a/ui/src/components/RetryTasksTable.tsx b/ui/src/components/RetryTasksTable.tsx index f59f562..f6fa58a 100644 --- a/ui/src/components/RetryTasksTable.tsx +++ b/ui/src/components/RetryTasksTable.tsx @@ -21,15 +21,18 @@ import Alert from "@material-ui/lab/Alert"; import AlertTitle from "@material-ui/lab/AlertTitle"; import SyntaxHighlighter from "react-syntax-highlighter"; import syntaxHighlightStyle from "react-syntax-highlighter/dist/esm/styles/hljs/github"; -import { listRetryTasksAsync } from "../actions/tasksActions"; +import { + listRetryTasksAsync, + deleteRetryTaskAsync, +} from "../actions/tasksActions"; import { AppState } from "../store"; -import { RetryTask } from "../api"; import TablePaginationActions, { defaultPageSize, rowsPerPageOptions, } from "./TablePaginationActions"; import { durationBefore } from "../timeutil"; import { usePolling } from "../hooks"; +import { RetryTaskExtended } from "../reducers/tasksReducer"; const useStyles = makeStyles({ table: { @@ -45,7 +48,7 @@ function mapStateToProps(state: AppState) { }; } -const mapDispatchToProps = { listRetryTasksAsync }; +const mapDispatchToProps = { listRetryTasksAsync, deleteRetryTaskAsync }; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -120,7 +123,13 @@ function RetryTasksTable(props: Props & ReduxProps) { {props.tasks.map((task) => ( - + { + props.deleteRetryTaskAsync(task.queue, task.key); + }} + /> ))} @@ -154,7 +163,7 @@ const useRowStyles = makeStyles({ }, }); -function Row(props: { task: RetryTask }) { +function Row(props: { task: RetryTaskExtended; onDeleteClick: () => void }) { const { task } = props; const [open, setOpen] = React.useState(false); const classes = useRowStyles(); @@ -179,7 +188,9 @@ function Row(props: { task: RetryTask }) { {task.retried} {task.max_retry} - + diff --git a/ui/src/reducers/queuesReducer.ts b/ui/src/reducers/queuesReducer.ts index 4372959..fb4a513 100644 --- a/ui/src/reducers/queuesReducer.ts +++ b/ui/src/reducers/queuesReducer.ts @@ -14,6 +14,7 @@ import { DELETE_QUEUE_SUCCESS, } from "../actions/queuesActions"; import { + DELETE_RETRY_TASK_SUCCESS, LIST_ACTIVE_TASKS_SUCCESS, LIST_DEAD_TASKS_SUCCESS, LIST_PENDING_TASKS_SUCCESS, @@ -147,6 +148,22 @@ function queuesReducer( return { ...state, data: newData }; } + case DELETE_RETRY_TASK_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + retry: queueInfo.currentStats.retry - 1, + }, + }; + }); + return { ...state, data: newData }; + } + default: return state; } diff --git a/ui/src/reducers/tasksReducer.ts b/ui/src/reducers/tasksReducer.ts index 16db987..f2a779a 100644 --- a/ui/src/reducers/tasksReducer.ts +++ b/ui/src/reducers/tasksReducer.ts @@ -18,6 +18,9 @@ import { CANCEL_ACTIVE_TASK_BEGIN, CANCEL_ACTIVE_TASK_SUCCESS, CANCEL_ACTIVE_TASK_ERROR, + DELETE_RETRY_TASK_BEGIN, + DELETE_RETRY_TASK_SUCCESS, + DELETE_RETRY_TASK_ERROR, } from "../actions/tasksActions"; import { ActiveTask, @@ -37,6 +40,12 @@ export interface ActiveTaskExtended extends ActiveTask { canceling: boolean; } +export interface RetryTaskExtended extends RetryTask { + // Indicates that a request has been sent for this + // task and awaiting for a response. + requestPending: boolean; +} + interface TasksState { activeTasks: { loading: boolean; @@ -56,7 +65,7 @@ interface TasksState { retryTasks: { loading: boolean; error: string; - data: RetryTask[]; + data: RetryTaskExtended[]; }; deadTasks: { loading: boolean; @@ -208,7 +217,10 @@ function tasksReducer( retryTasks: { loading: false, error: "", - data: action.payload.tasks, + data: action.payload.tasks.map((task) => ({ + ...task, + requestPending: false, + })), }, }; @@ -299,6 +311,45 @@ function tasksReducer( }, }; + case DELETE_RETRY_TASK_BEGIN: + return { + ...state, + retryTasks: { + ...state.retryTasks, + data: state.retryTasks.data.map((task) => { + if (task.key !== action.taskKey) { + return task; + } + return { ...task, requestPending: true }; + }), + }, + }; + + case DELETE_RETRY_TASK_SUCCESS: + return { + ...state, + retryTasks: { + ...state.retryTasks, + data: state.retryTasks.data.filter( + (task) => task.key !== action.taskKey + ), + }, + }; + + case DELETE_RETRY_TASK_ERROR: + return { + ...state, + retryTasks: { + ...state.retryTasks, + data: state.retryTasks.data.map((task) => { + if (task.key !== action.taskKey) { + return task; + } + return { ...task, requestPending: false }; + }), + }, + }; + default: return state; }