diff --git a/ui/src/actions/tasksActions.ts b/ui/src/actions/tasksActions.ts index c3e30fe..38cd205 100644 --- a/ui/src/actions/tasksActions.ts +++ b/ui/src/actions/tasksActions.ts @@ -1,6 +1,8 @@ import { cancelActiveTask, + deleteDeadTask, deleteRetryTask, + deleteScheduledTask, listActiveTasks, ListActiveTasksResponse, listDeadTasks, @@ -34,9 +36,15 @@ 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_SCHEDULED_TASK_BEGIN = "DELETE_SCHEDULED_TASK_BEGIN"; +export const DELETE_SCHEDULED_TASK_SUCCESS = "DELETE_SCHEDULED_TASK_SUCCESS"; +export const DELETE_SCHEDULED_TASK_ERROR = "DELETE_SCHEDULED_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"; +export const DELETE_DEAD_TASK_BEGIN = "DELETE_DEAD_TASK_BEGIN"; +export const DELETE_DEAD_TASK_SUCCESS = "DELETE_DEAD_TASK_SUCCESS"; +export const DELETE_DEAD_TASK_ERROR = "DELETE_DEAD_TASK_ERROR"; interface ListActiveTasksBeginAction { type: typeof LIST_ACTIVE_TASKS_BEGIN; @@ -142,6 +150,25 @@ interface CancelActiveTaskErrorAction { error: string; } +interface DeleteScheduledTaskBeginAction { + type: typeof DELETE_SCHEDULED_TASK_BEGIN; + queue: string; + taskKey: string; +} + +interface DeleteScheduledTaskSuccessAction { + type: typeof DELETE_SCHEDULED_TASK_SUCCESS; + queue: string; + taskKey: string; +} + +interface DeleteScheduledTaskErrorAction { + type: typeof DELETE_SCHEDULED_TASK_ERROR; + queue: string; + taskKey: string; + error: string; +} + interface DeleteRetryTaskBeginAction { type: typeof DELETE_RETRY_TASK_BEGIN; queue: string; @@ -160,6 +187,26 @@ interface DeleteRetryTaskErrorAction { taskKey: string; error: string; } + +interface DeleteDeadTaskBeginAction { + type: typeof DELETE_DEAD_TASK_BEGIN; + queue: string; + taskKey: string; +} + +interface DeleteDeadTaskSuccessAction { + type: typeof DELETE_DEAD_TASK_SUCCESS; + queue: string; + taskKey: string; +} + +interface DeleteDeadTaskErrorAction { + type: typeof DELETE_DEAD_TASK_ERROR; + queue: string; + taskKey: string; + error: string; +} + // Union of all tasks related action types. export type TasksActionTypes = | ListActiveTasksBeginAction @@ -180,9 +227,15 @@ export type TasksActionTypes = | CancelActiveTaskBeginAction | CancelActiveTaskSuccessAction | CancelActiveTaskErrorAction + | DeleteScheduledTaskBeginAction + | DeleteScheduledTaskSuccessAction + | DeleteScheduledTaskErrorAction | DeleteRetryTaskBeginAction | DeleteRetryTaskSuccessAction - | DeleteRetryTaskErrorAction; + | DeleteRetryTaskErrorAction + | DeleteDeadTaskBeginAction + | DeleteDeadTaskSuccessAction + | DeleteDeadTaskErrorAction; export function listActiveTasksAsync( qname: string, @@ -316,6 +369,24 @@ export function cancelActiveTaskAsync(queue: string, taskId: string) { }; } +export function deleteScheduledTaskAsync(queue: string, taskKey: string) { + return async (dispatch: Dispatch) => { + dispatch({ type: DELETE_SCHEDULED_TASK_BEGIN, queue, taskKey }); + try { + await deleteScheduledTask(queue, taskKey); + dispatch({ type: DELETE_SCHEDULED_TASK_SUCCESS, queue, taskKey }); + } catch (error) { + console.error("deleteScheduledTaskAsync: ", error); + dispatch({ + type: DELETE_SCHEDULED_TASK_ERROR, + error: `Could not delete task: ${taskKey}`, + queue, + taskKey, + }); + } + }; +} + export function deleteRetryTaskAsync(queue: string, taskKey: string) { return async (dispatch: Dispatch) => { dispatch({ type: DELETE_RETRY_TASK_BEGIN, queue, taskKey }); @@ -333,3 +404,21 @@ export function deleteRetryTaskAsync(queue: string, taskKey: string) { } }; } + +export function deleteDeadTaskAsync(queue: string, taskKey: string) { + return async (dispatch: Dispatch) => { + dispatch({ type: DELETE_DEAD_TASK_BEGIN, queue, taskKey }); + try { + await deleteDeadTask(queue, taskKey); + dispatch({ type: DELETE_DEAD_TASK_SUCCESS, queue, taskKey }); + } catch (error) { + console.error("deleteDeadTaskAsync: ", error); + dispatch({ + type: DELETE_DEAD_TASK_ERROR, + error: `Could not delete task: ${taskKey}`, + queue, + taskKey, + }); + } + }; +} diff --git a/ui/src/api.ts b/ui/src/api.ts index d51f2d6..053e47c 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -243,6 +243,16 @@ export async function listDeadTasks( return resp.data; } +export async function deleteScheduledTask( + qname: string, + taskKey: string +): Promise { + await axios({ + method: "delete", + url: `${BASE_URL}/queues/${qname}/scheduled_tasks/${taskKey}`, + }); +} + export async function deleteRetryTask( qname: string, taskKey: string @@ -253,6 +263,16 @@ export async function deleteRetryTask( }); } +export async function deleteDeadTask( + qname: string, + taskKey: string +): Promise { + await axios({ + method: "delete", + url: `${BASE_URL}/queues/${qname}/dead_tasks/${taskKey}`, + }); +} + export async function listSchedulerEntries(): Promise { const resp = await axios({ method: "get", diff --git a/ui/src/components/DeadTasksTable.tsx b/ui/src/components/DeadTasksTable.tsx index a8ed6ac..2e25e13 100644 --- a/ui/src/components/DeadTasksTable.tsx +++ b/ui/src/components/DeadTasksTable.tsx @@ -22,14 +22,17 @@ 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 { AppState } from "../store"; -import { listDeadTasksAsync } from "../actions/tasksActions"; -import { DeadTask } from "../api"; +import { + listDeadTasksAsync, + deleteDeadTaskAsync, +} from "../actions/tasksActions"; import TablePaginationActions, { defaultPageSize, rowsPerPageOptions, } from "./TablePaginationActions"; import { timeAgo } from "../timeutil"; import { usePolling } from "../hooks"; +import { DeadTaskExtended } from "../reducers/tasksReducer"; const useStyles = makeStyles({ table: { @@ -53,7 +56,7 @@ function mapStateToProps(state: AppState) { }; } -const mapDispatchToProps = { listDeadTasksAsync }; +const mapDispatchToProps = { listDeadTasksAsync, deleteDeadTaskAsync }; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -126,7 +129,13 @@ function DeadTasksTable(props: Props & ReduxProps) { {props.tasks.map((task) => ( - + { + props.deleteDeadTaskAsync(queue, task.key); + }} + /> ))} @@ -152,7 +161,12 @@ function DeadTasksTable(props: Props & ReduxProps) { ); } -function Row(props: { task: DeadTask }) { +interface RowProps { + task: DeadTaskExtended; + onDeleteClick: () => void; +} + +function Row(props: RowProps) { const { task } = props; const [open, setOpen] = React.useState(false); const classes = useRowStyles(); @@ -175,7 +189,9 @@ function Row(props: { task: DeadTask }) { {timeAgo(task.last_failed_at)} {task.error_message} - + diff --git a/ui/src/components/RetryTasksTable.tsx b/ui/src/components/RetryTasksTable.tsx index f6fa58a..97170c7 100644 --- a/ui/src/components/RetryTasksTable.tsx +++ b/ui/src/components/RetryTasksTable.tsx @@ -163,7 +163,12 @@ const useRowStyles = makeStyles({ }, }); -function Row(props: { task: RetryTaskExtended; onDeleteClick: () => void }) { +interface RowProps { + task: RetryTaskExtended; + onDeleteClick: () => void; +} + +function Row(props: RowProps) { const { task } = props; const [open, setOpen] = React.useState(false); const classes = useRowStyles(); diff --git a/ui/src/components/ScheduledTasksTable.tsx b/ui/src/components/ScheduledTasksTable.tsx index 2527355..ed6954f 100644 --- a/ui/src/components/ScheduledTasksTable.tsx +++ b/ui/src/components/ScheduledTasksTable.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 { listScheduledTasksAsync } from "../actions/tasksActions"; +import { + listScheduledTasksAsync, + deleteScheduledTaskAsync, +} from "../actions/tasksActions"; import { AppState } from "../store"; -import { ScheduledTask } from "../api"; import TablePaginationActions, { defaultPageSize, rowsPerPageOptions, } from "./TablePaginationActions"; import { durationBefore } from "../timeutil"; import { usePolling } from "../hooks"; +import { ScheduledTaskExtended } from "../reducers/tasksReducer"; const useStyles = makeStyles({ table: { @@ -45,7 +48,10 @@ function mapStateToProps(state: AppState) { }; } -const mapDispatchToProps = { listScheduledTasksAsync }; +const mapDispatchToProps = { + listScheduledTasksAsync, + deleteScheduledTaskAsync, +}; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -117,7 +123,13 @@ function ScheduledTasksTable(props: Props & ReduxProps) { {props.tasks.map((task) => ( - + { + props.deleteScheduledTaskAsync(queue, task.key); + }} + /> ))} @@ -151,7 +163,12 @@ const useRowStyles = makeStyles({ }, }); -function Row(props: { task: ScheduledTask }) { +interface RowProps { + task: ScheduledTaskExtended; + onDeleteClick: () => void; +} + +function Row(props: RowProps) { const { task } = props; const [open, setOpen] = React.useState(false); const classes = useRowStyles(); @@ -173,7 +190,9 @@ function Row(props: { task: ScheduledTask }) { {task.type} {durationBefore(task.next_process_at)} - + diff --git a/ui/src/reducers/queuesReducer.ts b/ui/src/reducers/queuesReducer.ts index fb4a513..fa9bbd3 100644 --- a/ui/src/reducers/queuesReducer.ts +++ b/ui/src/reducers/queuesReducer.ts @@ -14,7 +14,9 @@ import { DELETE_QUEUE_SUCCESS, } from "../actions/queuesActions"; import { + DELETE_DEAD_TASK_SUCCESS, DELETE_RETRY_TASK_SUCCESS, + DELETE_SCHEDULED_TASK_SUCCESS, LIST_ACTIVE_TASKS_SUCCESS, LIST_DEAD_TASKS_SUCCESS, LIST_PENDING_TASKS_SUCCESS, @@ -148,6 +150,22 @@ function queuesReducer( return { ...state, data: newData }; } + case DELETE_SCHEDULED_TASK_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + scheduled: queueInfo.currentStats.scheduled - 1, + }, + }; + }); + return { ...state, data: newData }; + } + case DELETE_RETRY_TASK_SUCCESS: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { @@ -164,6 +182,22 @@ function queuesReducer( return { ...state, data: newData }; } + case DELETE_DEAD_TASK_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + dead: queueInfo.currentStats.dead - 1, + }, + }; + }); + return { ...state, data: newData }; + } + default: return state; } diff --git a/ui/src/reducers/tasksReducer.ts b/ui/src/reducers/tasksReducer.ts index f2a779a..b0190f1 100644 --- a/ui/src/reducers/tasksReducer.ts +++ b/ui/src/reducers/tasksReducer.ts @@ -21,6 +21,12 @@ import { DELETE_RETRY_TASK_BEGIN, DELETE_RETRY_TASK_SUCCESS, DELETE_RETRY_TASK_ERROR, + DELETE_SCHEDULED_TASK_BEGIN, + DELETE_SCHEDULED_TASK_SUCCESS, + DELETE_SCHEDULED_TASK_ERROR, + DELETE_DEAD_TASK_BEGIN, + DELETE_DEAD_TASK_SUCCESS, + DELETE_DEAD_TASK_ERROR, } from "../actions/tasksActions"; import { ActiveTask, @@ -40,12 +46,24 @@ export interface ActiveTaskExtended extends ActiveTask { canceling: boolean; } +export interface ScheduledTaskExtended extends ScheduledTask { + // Indicates that a request has been sent for this + // task and awaiting for a response. + requestPending: boolean; +} + export interface RetryTaskExtended extends RetryTask { // Indicates that a request has been sent for this // task and awaiting for a response. requestPending: boolean; } +export interface DeadTaskExtended extends DeadTask { + // Indicates that a request has been sent for this + // task and awaiting for a response. + requestPending: boolean; +} + interface TasksState { activeTasks: { loading: boolean; @@ -60,7 +78,7 @@ interface TasksState { scheduledTasks: { loading: boolean; error: string; - data: ScheduledTask[]; + data: ScheduledTaskExtended[]; }; retryTasks: { loading: boolean; @@ -70,7 +88,7 @@ interface TasksState { deadTasks: { loading: boolean; error: string; - data: DeadTask[]; + data: DeadTaskExtended[]; }; } @@ -187,7 +205,10 @@ function tasksReducer( scheduledTasks: { loading: false, error: "", - data: action.payload.tasks, + data: action.payload.tasks.map((task) => ({ + ...task, + requestPending: false, + })), }, }; @@ -250,7 +271,10 @@ function tasksReducer( deadTasks: { loading: false, error: "", - data: action.payload.tasks, + data: action.payload.tasks.map((task) => ({ + ...task, + requestPending: false, + })), }, }; @@ -311,6 +335,45 @@ function tasksReducer( }, }; + case DELETE_SCHEDULED_TASK_BEGIN: + return { + ...state, + scheduledTasks: { + ...state.scheduledTasks, + data: state.scheduledTasks.data.map((task) => { + if (task.key !== action.taskKey) { + return task; + } + return { ...task, requestPending: true }; + }), + }, + }; + + case DELETE_SCHEDULED_TASK_SUCCESS: + return { + ...state, + scheduledTasks: { + ...state.scheduledTasks, + data: state.scheduledTasks.data.filter( + (task) => task.key !== action.taskKey + ), + }, + }; + + case DELETE_SCHEDULED_TASK_ERROR: + return { + ...state, + scheduledTasks: { + ...state.scheduledTasks, + data: state.scheduledTasks.data.map((task) => { + if (task.key !== action.taskKey) { + return task; + } + return { ...task, requestPending: false }; + }), + }, + }; + case DELETE_RETRY_TASK_BEGIN: return { ...state, @@ -350,6 +413,45 @@ function tasksReducer( }, }; + case DELETE_DEAD_TASK_BEGIN: + return { + ...state, + deadTasks: { + ...state.deadTasks, + data: state.deadTasks.data.map((task) => { + if (task.key !== action.taskKey) { + return task; + } + return { ...task, requestPending: true }; + }), + }, + }; + + case DELETE_DEAD_TASK_SUCCESS: + return { + ...state, + deadTasks: { + ...state.deadTasks, + data: state.deadTasks.data.filter( + (task) => task.key !== action.taskKey + ), + }, + }; + + case DELETE_DEAD_TASK_ERROR: + return { + ...state, + deadTasks: { + ...state.deadTasks, + data: state.deadTasks.data.map((task) => { + if (task.key !== action.taskKey) { + return task; + } + return { ...task, requestPending: false }; + }), + }, + }; + default: return state; }