mirror of
https://github.com/hibiken/asynqmon.git
synced 2025-02-23 20:30:12 +08:00
Add batch delete button to DeadTasksTable
This commit is contained in:
parent
62780e201e
commit
ef60304015
@ -229,7 +229,7 @@ func newDeleteAllDeadTasksHandlerFunc(inspector *asynq.Inspector) http.HandlerFu
|
|||||||
|
|
||||||
// request body used for all batch delete tasks endpoints.
|
// request body used for all batch delete tasks endpoints.
|
||||||
type batchDeleteTasksRequest struct {
|
type batchDeleteTasksRequest struct {
|
||||||
taskKeys []string `json:"task_keys"`
|
TaskKeys []string `json:"task_keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Redis does not have any rollback mechanism, so it's possible
|
// Note: Redis does not have any rollback mechanism, so it's possible
|
||||||
@ -238,10 +238,10 @@ type batchDeleteTasksRequest struct {
|
|||||||
// and a list of failed keys.
|
// and a list of failed keys.
|
||||||
type batchDeleteTasksResponse struct {
|
type batchDeleteTasksResponse struct {
|
||||||
// task keys that were successfully deleted.
|
// task keys that were successfully deleted.
|
||||||
deletedKeys []string `json:"deleted_keys"`
|
DeletedKeys []string `json:"deleted_keys"`
|
||||||
|
|
||||||
// task keys that were not deleted.
|
// task keys that were not deleted.
|
||||||
failedKeys []string `json:"failed_keys"`
|
FailedKeys []string `json:"failed_keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximum request body size in bytes.
|
// Maximum request body size in bytes.
|
||||||
@ -261,13 +261,17 @@ func newBatchDeleteDeadTasksHandlerFunc(inspector *asynq.Inspector) http.Handler
|
|||||||
}
|
}
|
||||||
|
|
||||||
qname := mux.Vars(r)["qname"]
|
qname := mux.Vars(r)["qname"]
|
||||||
var resp batchDeleteTasksResponse
|
resp := batchDeleteTasksResponse{
|
||||||
for _, key := range req.taskKeys {
|
// avoid null in the json response
|
||||||
|
DeletedKeys: make([]string, 0),
|
||||||
|
FailedKeys: make([]string, 0),
|
||||||
|
}
|
||||||
|
for _, key := range req.TaskKeys {
|
||||||
if err := inspector.DeleteTaskByKey(qname, key); err != nil {
|
if err := inspector.DeleteTaskByKey(qname, key); err != nil {
|
||||||
log.Printf("error: could not delete task with key %q: %v", key, err)
|
log.Printf("error: could not delete task with key %q: %v", key, err)
|
||||||
resp.failedKeys = append(resp.failedKeys, key)
|
resp.FailedKeys = append(resp.FailedKeys, key)
|
||||||
} else {
|
} else {
|
||||||
resp.deletedKeys = append(resp.deletedKeys, key)
|
resp.DeletedKeys = append(resp.DeletedKeys, key)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
|
batchDeleteDeadTasks,
|
||||||
|
BatchDeleteTasksResponse,
|
||||||
cancelActiveTask,
|
cancelActiveTask,
|
||||||
deleteDeadTask,
|
deleteDeadTask,
|
||||||
deleteRetryTask,
|
deleteRetryTask,
|
||||||
@ -45,6 +47,10 @@ 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_BEGIN = "DELETE_DEAD_TASK_BEGIN";
|
||||||
export const DELETE_DEAD_TASK_SUCCESS = "DELETE_DEAD_TASK_SUCCESS";
|
export const DELETE_DEAD_TASK_SUCCESS = "DELETE_DEAD_TASK_SUCCESS";
|
||||||
export const DELETE_DEAD_TASK_ERROR = "DELETE_DEAD_TASK_ERROR";
|
export const DELETE_DEAD_TASK_ERROR = "DELETE_DEAD_TASK_ERROR";
|
||||||
|
export const BATCH_DELETE_DEAD_TASKS_BEGIN = "BATCH_DELETE_DEAD_TASKS_BEGIN";
|
||||||
|
export const BATCH_DELETE_DEAD_TASKS_SUCCESS =
|
||||||
|
"BATCH_DELETE_DEAD_TASKS_SUCCESS";
|
||||||
|
export const BATCH_DELETE_DEAD_TASKS_ERROR = "BATCH_DELETE_DEAD_TASKS_ERROR";
|
||||||
|
|
||||||
interface ListActiveTasksBeginAction {
|
interface ListActiveTasksBeginAction {
|
||||||
type: typeof LIST_ACTIVE_TASKS_BEGIN;
|
type: typeof LIST_ACTIVE_TASKS_BEGIN;
|
||||||
@ -207,6 +213,25 @@ interface DeleteDeadTaskErrorAction {
|
|||||||
error: string;
|
error: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface BatchDeleteDeadTasksBeginAction {
|
||||||
|
type: typeof BATCH_DELETE_DEAD_TASKS_BEGIN;
|
||||||
|
queue: string;
|
||||||
|
taskKeys: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchDeleteDeadTasksSuccessAction {
|
||||||
|
type: typeof BATCH_DELETE_DEAD_TASKS_SUCCESS;
|
||||||
|
queue: string;
|
||||||
|
payload: BatchDeleteTasksResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BatchDeleteDeadTasksErrorAction {
|
||||||
|
type: typeof BATCH_DELETE_DEAD_TASKS_ERROR;
|
||||||
|
queue: string;
|
||||||
|
taskKeys: string[];
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Union of all tasks related action types.
|
// Union of all tasks related action types.
|
||||||
export type TasksActionTypes =
|
export type TasksActionTypes =
|
||||||
| ListActiveTasksBeginAction
|
| ListActiveTasksBeginAction
|
||||||
@ -235,7 +260,10 @@ export type TasksActionTypes =
|
|||||||
| DeleteRetryTaskErrorAction
|
| DeleteRetryTaskErrorAction
|
||||||
| DeleteDeadTaskBeginAction
|
| DeleteDeadTaskBeginAction
|
||||||
| DeleteDeadTaskSuccessAction
|
| DeleteDeadTaskSuccessAction
|
||||||
| DeleteDeadTaskErrorAction;
|
| DeleteDeadTaskErrorAction
|
||||||
|
| BatchDeleteDeadTasksBeginAction
|
||||||
|
| BatchDeleteDeadTasksSuccessAction
|
||||||
|
| BatchDeleteDeadTasksErrorAction;
|
||||||
|
|
||||||
export function listActiveTasksAsync(
|
export function listActiveTasksAsync(
|
||||||
qname: string,
|
qname: string,
|
||||||
@ -422,3 +450,25 @@ export function deleteDeadTaskAsync(queue: string, taskKey: string) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function batchDeleteDeadTasksAsync(queue: string, taskKeys: string[]) {
|
||||||
|
return async (dispatch: Dispatch<TasksActionTypes>) => {
|
||||||
|
dispatch({ type: BATCH_DELETE_DEAD_TASKS_BEGIN, queue, taskKeys });
|
||||||
|
try {
|
||||||
|
const response = await batchDeleteDeadTasks(queue, taskKeys);
|
||||||
|
dispatch({
|
||||||
|
type: BATCH_DELETE_DEAD_TASKS_SUCCESS,
|
||||||
|
queue: queue,
|
||||||
|
payload: response,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("batchDeleteDeadTasksAsync: ", error);
|
||||||
|
dispatch({
|
||||||
|
type: BATCH_DELETE_DEAD_TASKS_ERROR,
|
||||||
|
error: `Could not batch delete tasks: ${taskKeys}`,
|
||||||
|
queue,
|
||||||
|
taskKeys,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -41,6 +41,11 @@ export interface ListSchedulerEntriesResponse {
|
|||||||
entries: SchedulerEntry[];
|
entries: SchedulerEntry[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BatchDeleteTasksResponse {
|
||||||
|
deleted_keys: string[];
|
||||||
|
failed_keys: string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface Queue {
|
export interface Queue {
|
||||||
queue: string;
|
queue: string;
|
||||||
paused: boolean;
|
paused: boolean;
|
||||||
@ -273,6 +278,21 @@ export async function deleteDeadTask(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function batchDeleteDeadTasks(
|
||||||
|
qname: string,
|
||||||
|
taskKeys: string[]
|
||||||
|
): Promise<BatchDeleteTasksResponse> {
|
||||||
|
const resp = await axios({
|
||||||
|
method: "post",
|
||||||
|
url: `${BASE_URL}/queues/${qname}/dead_tasks:batch_delete`,
|
||||||
|
data: {
|
||||||
|
task_keys: taskKeys,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log("debug: response:", resp);
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
export async function listSchedulerEntries(): Promise<ListSchedulerEntriesResponse> {
|
export async function listSchedulerEntries(): Promise<ListSchedulerEntriesResponse> {
|
||||||
const resp = await axios({
|
const resp = await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
@ -28,6 +28,7 @@ import { AppState } from "../store";
|
|||||||
import {
|
import {
|
||||||
listDeadTasksAsync,
|
listDeadTasksAsync,
|
||||||
deleteDeadTaskAsync,
|
deleteDeadTaskAsync,
|
||||||
|
batchDeleteDeadTasksAsync,
|
||||||
} from "../actions/tasksActions";
|
} from "../actions/tasksActions";
|
||||||
import TablePaginationActions, {
|
import TablePaginationActions, {
|
||||||
defaultPageSize,
|
defaultPageSize,
|
||||||
@ -61,11 +62,16 @@ function mapStateToProps(state: AppState) {
|
|||||||
return {
|
return {
|
||||||
loading: state.tasks.deadTasks.loading,
|
loading: state.tasks.deadTasks.loading,
|
||||||
tasks: state.tasks.deadTasks.data,
|
tasks: state.tasks.deadTasks.data,
|
||||||
|
batchActionPending: state.tasks.deadTasks.batchActionPending,
|
||||||
pollInterval: state.settings.pollInterval,
|
pollInterval: state.settings.pollInterval,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapDispatchToProps = { listDeadTasksAsync, deleteDeadTaskAsync };
|
const mapDispatchToProps = {
|
||||||
|
listDeadTasksAsync,
|
||||||
|
deleteDeadTaskAsync,
|
||||||
|
batchDeleteDeadTasksAsync,
|
||||||
|
};
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
|
|
||||||
@ -81,7 +87,7 @@ function DeadTasksTable(props: Props & ReduxProps) {
|
|||||||
const classes = useStyles();
|
const classes = useStyles();
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [pageSize, setPageSize] = useState(defaultPageSize);
|
const [pageSize, setPageSize] = useState(defaultPageSize);
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
const [selectedKeys, setSelectedKeys] = useState<string[]>([]);
|
||||||
|
|
||||||
const handleChangePage = (
|
const handleChangePage = (
|
||||||
event: React.MouseEvent<HTMLButtonElement> | null,
|
event: React.MouseEvent<HTMLButtonElement> | null,
|
||||||
@ -99,10 +105,10 @@ function DeadTasksTable(props: Props & ReduxProps) {
|
|||||||
|
|
||||||
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (event.target.checked) {
|
if (event.target.checked) {
|
||||||
const newSelected = props.tasks.map((t) => t.id);
|
const newSelected = props.tasks.map((t) => t.key);
|
||||||
setSelected(newSelected);
|
setSelectedKeys(newSelected);
|
||||||
} else {
|
} else {
|
||||||
setSelected([]);
|
setSelectedKeys([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,7 +138,7 @@ function DeadTasksTable(props: Props & ReduxProps) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const rowCount = props.tasks.length;
|
const rowCount = props.tasks.length;
|
||||||
const numSelected = selected.length;
|
const numSelected = selectedKeys.length;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className={classes.actionsContainer}>
|
<div className={classes.actionsContainer}>
|
||||||
@ -147,7 +153,16 @@ function DeadTasksTable(props: Props & ReduxProps) {
|
|||||||
>
|
>
|
||||||
<Button>Run</Button>
|
<Button>Run</Button>
|
||||||
<Button>Kill</Button>
|
<Button>Kill</Button>
|
||||||
<Button>Delete</Button>
|
<Button
|
||||||
|
disabled={props.batchActionPending}
|
||||||
|
onClick={() =>
|
||||||
|
props
|
||||||
|
.batchDeleteDeadTasksAsync(queue, selectedKeys)
|
||||||
|
.then(() => setSelectedKeys([]))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -176,14 +191,16 @@ function DeadTasksTable(props: Props & ReduxProps) {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{props.tasks.map((task) => (
|
{props.tasks.map((task) => (
|
||||||
<Row
|
<Row
|
||||||
key={task.id}
|
key={task.key}
|
||||||
task={task}
|
task={task}
|
||||||
isSelected={selected.includes(task.id)}
|
isSelected={selectedKeys.includes(task.key)}
|
||||||
onSelectChange={(checked: boolean) => {
|
onSelectChange={(checked: boolean) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setSelected(selected.concat(task.id));
|
setSelectedKeys(selectedKeys.concat(task.key));
|
||||||
} else {
|
} else {
|
||||||
setSelected(selected.filter((id) => id !== task.id));
|
setSelectedKeys(
|
||||||
|
selectedKeys.filter((key) => key !== task.key)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onDeleteClick={() => {
|
onDeleteClick={() => {
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
DELETE_QUEUE_SUCCESS,
|
DELETE_QUEUE_SUCCESS,
|
||||||
} from "../actions/queuesActions";
|
} from "../actions/queuesActions";
|
||||||
import {
|
import {
|
||||||
|
BATCH_DELETE_DEAD_TASKS_SUCCESS,
|
||||||
DELETE_DEAD_TASK_SUCCESS,
|
DELETE_DEAD_TASK_SUCCESS,
|
||||||
DELETE_RETRY_TASK_SUCCESS,
|
DELETE_RETRY_TASK_SUCCESS,
|
||||||
DELETE_SCHEDULED_TASK_SUCCESS,
|
DELETE_SCHEDULED_TASK_SUCCESS,
|
||||||
@ -198,6 +199,23 @@ function queuesReducer(
|
|||||||
return { ...state, data: newData };
|
return { ...state, data: newData };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case BATCH_DELETE_DEAD_TASKS_SUCCESS: {
|
||||||
|
const newData = state.data.map((queueInfo) => {
|
||||||
|
if (queueInfo.name !== action.queue) {
|
||||||
|
return queueInfo;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...queueInfo,
|
||||||
|
currentStats: {
|
||||||
|
...queueInfo.currentStats,
|
||||||
|
dead:
|
||||||
|
queueInfo.currentStats.dead - action.payload.deleted_keys.length,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return { ...state, data: newData };
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
SnackbarActionTypes,
|
SnackbarActionTypes,
|
||||||
} from "../actions/snackbarActions";
|
} from "../actions/snackbarActions";
|
||||||
import {
|
import {
|
||||||
|
BATCH_DELETE_DEAD_TASKS_SUCCESS,
|
||||||
DELETE_DEAD_TASK_SUCCESS,
|
DELETE_DEAD_TASK_SUCCESS,
|
||||||
DELETE_RETRY_TASK_SUCCESS,
|
DELETE_RETRY_TASK_SUCCESS,
|
||||||
DELETE_SCHEDULED_TASK_SUCCESS,
|
DELETE_SCHEDULED_TASK_SUCCESS,
|
||||||
@ -53,6 +54,14 @@ function snackbarReducer(
|
|||||||
message: `Dead task ${action.taskKey} deleted`,
|
message: `Dead task ${action.taskKey} deleted`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case BATCH_DELETE_DEAD_TASKS_SUCCESS: {
|
||||||
|
const n = action.payload.deleted_keys.length;
|
||||||
|
return {
|
||||||
|
isOpen: true,
|
||||||
|
message: `${n} Dead ${n === 1 ? "task" : "tasks"} deleted`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,9 @@ import {
|
|||||||
DELETE_DEAD_TASK_BEGIN,
|
DELETE_DEAD_TASK_BEGIN,
|
||||||
DELETE_DEAD_TASK_SUCCESS,
|
DELETE_DEAD_TASK_SUCCESS,
|
||||||
DELETE_DEAD_TASK_ERROR,
|
DELETE_DEAD_TASK_ERROR,
|
||||||
|
BATCH_DELETE_DEAD_TASKS_BEGIN,
|
||||||
|
BATCH_DELETE_DEAD_TASKS_SUCCESS,
|
||||||
|
BATCH_DELETE_DEAD_TASKS_ERROR,
|
||||||
} from "../actions/tasksActions";
|
} from "../actions/tasksActions";
|
||||||
import {
|
import {
|
||||||
ActiveTask,
|
ActiveTask,
|
||||||
@ -87,6 +90,7 @@ interface TasksState {
|
|||||||
};
|
};
|
||||||
deadTasks: {
|
deadTasks: {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
batchActionPending: boolean;
|
||||||
error: string;
|
error: string;
|
||||||
data: DeadTaskExtended[];
|
data: DeadTaskExtended[];
|
||||||
};
|
};
|
||||||
@ -115,6 +119,7 @@ const initialState: TasksState = {
|
|||||||
},
|
},
|
||||||
deadTasks: {
|
deadTasks: {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
batchActionPending: false,
|
||||||
error: "",
|
error: "",
|
||||||
data: [],
|
data: [],
|
||||||
},
|
},
|
||||||
@ -269,6 +274,7 @@ function tasksReducer(
|
|||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
deadTasks: {
|
deadTasks: {
|
||||||
|
...state.deadTasks,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: "",
|
error: "",
|
||||||
data: action.payload.tasks.map((task) => ({
|
data: action.payload.tasks.map((task) => ({
|
||||||
@ -452,6 +458,38 @@ function tasksReducer(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
case BATCH_DELETE_DEAD_TASKS_BEGIN:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deadTasks: {
|
||||||
|
...state.deadTasks,
|
||||||
|
batchActionPending: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
case BATCH_DELETE_DEAD_TASKS_SUCCESS: {
|
||||||
|
const newData = state.deadTasks.data.filter(
|
||||||
|
(task) => !action.payload.deleted_keys.includes(task.key)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deadTasks: {
|
||||||
|
...state.deadTasks,
|
||||||
|
batchActionPending: false,
|
||||||
|
data: newData,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
case BATCH_DELETE_DEAD_TASKS_ERROR:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
deadTasks: {
|
||||||
|
...state.deadTasks,
|
||||||
|
batchActionPending: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user