From caae17a23067b4bdb1ecf903b2f6ebc4bf4a75e4 Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Thu, 21 Jan 2021 16:15:24 -0800 Subject: [PATCH] Add delete and archive action buttons to PendingTasksTable --- ui/src/components/PendingTasksTable.tsx | 282 ++++++++++++++++++++---- ui/src/reducers/queuesReducer.ts | 112 ++++++++++ ui/src/reducers/tasksReducer.ts | 13 +- 3 files changed, 360 insertions(+), 47 deletions(-) diff --git a/ui/src/components/PendingTasksTable.tsx b/ui/src/components/PendingTasksTable.tsx index 77fc5b5..ab76430 100644 --- a/ui/src/components/PendingTasksTable.tsx +++ b/ui/src/components/PendingTasksTable.tsx @@ -1,4 +1,5 @@ import React, { useCallback, useState } from "react"; +import clsx from "clsx"; import { connect, ConnectedProps } from "react-redux"; import { makeStyles } from "@material-ui/core/styles"; import Table from "@material-ui/core/Table"; @@ -12,6 +13,7 @@ import TableRow from "@material-ui/core/TableRow"; import Tooltip from "@material-ui/core/Tooltip"; import Paper from "@material-ui/core/Paper"; import Box from "@material-ui/core/Box"; +import Checkbox from "@material-ui/core/Checkbox"; import Collapse from "@material-ui/core/Collapse"; import IconButton from "@material-ui/core/IconButton"; import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; @@ -19,17 +21,29 @@ import KeyboardArrowDownIcon from "@material-ui/icons/KeyboardArrowDown"; import Typography from "@material-ui/core/Typography"; import TableFooter from "@material-ui/core/TableFooter"; import TablePagination from "@material-ui/core/TablePagination"; +import DeleteIcon from "@material-ui/icons/Delete"; +import ArchiveIcon from "@material-ui/icons/Archive"; +import MoreHorizIcon from "@material-ui/icons/MoreHoriz"; import SyntaxHighlighter from "./SyntaxHighlighter"; import TablePaginationActions, { defaultPageSize, rowsPerPageOptions, } from "./TablePaginationActions"; -import { listPendingTasksAsync } from "../actions/tasksActions"; +import TableActions from "./TableActions"; +import { + listPendingTasksAsync, + deletePendingTaskAsync, + batchDeletePendingTasksAsync, + deleteAllPendingTasksAsync, + archivePendingTaskAsync, + batchArchivePendingTasksAsync, + archiveAllPendingTasksAsync, +} from "../actions/tasksActions"; import { AppState } from "../store"; -import { PendingTask } from "../api"; import { usePolling } from "../hooks"; import { uuidPrefix } from "../utils"; import { TableColumn } from "../types/table"; +import { PendingTaskExtended } from "../reducers/tasksReducer"; const useStyles = makeStyles((theme) => ({ table: { @@ -44,11 +58,21 @@ function mapStateToProps(state: AppState) { return { loading: state.tasks.pendingTasks.loading, tasks: state.tasks.pendingTasks.data, + batchActionPending: state.tasks.pendingTasks.batchActionPending, + allActionPending: state.tasks.pendingTasks.allActionPending, pollInterval: state.settings.pollInterval, }; } -const mapDispatchToProps = { listPendingTasksAsync }; +const mapDispatchToProps = { + listPendingTasksAsync, + deletePendingTaskAsync, + batchDeletePendingTasksAsync, + deleteAllPendingTasksAsync, + archivePendingTaskAsync, + batchArchivePendingTasksAsync, + archiveAllPendingTasksAsync, +}; const connector = connect(mapStateToProps, mapDispatchToProps); @@ -64,6 +88,8 @@ function PendingTasksTable(props: Props & ReduxProps) { const classes = useStyles(); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(defaultPageSize); + const [selectedKeys, setSelectedKeys] = useState([]); + const [activeTaskId, setActiveTaskId] = useState(""); const handleChangePage = ( event: React.MouseEvent | null, @@ -79,6 +105,35 @@ function PendingTasksTable(props: Props & ReduxProps) { setPage(0); }; + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = props.tasks.map((t) => t.key); + setSelectedKeys(newSelected); + } else { + setSelectedKeys([]); + } + }; + + const handleDeleteAllClick = () => { + props.deleteAllPendingTasksAsync(queue); + }; + + const handleArchiveAllClick = () => { + props.archiveAllPendingTasksAsync(queue); + }; + + const handleBatchDeleteClick = () => { + props + .batchDeletePendingTasksAsync(queue, selectedKeys) + .then(() => setSelectedKeys([])); + }; + + const handleBatchArchiveClick = () => { + props + .batchArchivePendingTasksAsync(queue, selectedKeys) + .then(() => setSelectedKeys([])); + }; + const fetchData = useCallback(() => { const pageOpts = { page: page + 1, size: pageSize }; listPendingTasksAsync(queue, pageOpts); @@ -99,54 +154,124 @@ function PendingTasksTable(props: Props & ReduxProps) { { key: "icon", label: "", align: "left" }, { key: "id", label: "ID", align: "left" }, { key: "type", label: "Type", align: "left" }, + { key: "actions", label: "Actions", align: "center" }, ]; + const rowCount = props.tasks.length; + const numSelected = selectedKeys.length; return ( - - - - - {columns.map((col) => ( +
+ 0} + iconButtonActions={[ + { + tooltip: "Delete", + icon: , + onClick: handleBatchDeleteClick, + disabled: props.batchActionPending, + }, + { + tooltip: "Archive", + icon: , + onClick: handleBatchArchiveClick, + disabled: props.batchActionPending, + }, + ]} + menuItemActions={[ + { + label: "Delete All", + onClick: handleDeleteAllClick, + disabled: props.allActionPending, + }, + { + label: "Archive All", + onClick: handleArchiveAllClick, + disabled: props.allActionPending, + }, + ]} + /> + +
+ + - {col.label} + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={handleSelectAllClick} + inputProps={{ + "aria-label": "select all tasks shown in the table", + }} + /> + {columns.map((col) => ( + + {col.label} + + ))} + + + + {props.tasks.map((task) => ( + { + if (checked) { + setSelectedKeys(selectedKeys.concat(task.key)); + } else { + setSelectedKeys( + selectedKeys.filter((key) => key !== task.key) + ); + } + }} + allActionPending={props.allActionPending} + onDeleteClick={() => + props.deletePendingTaskAsync(queue, task.key) + } + onArchiveClick={() => { + props.archivePendingTaskAsync(queue, task.key); + }} + onActionCellEnter={() => setActiveTaskId(task.id)} + onActionCellLeave={() => setActiveTaskId("")} + showActions={activeTaskId === task.id} + /> ))} - - - - {props.tasks.map((task) => ( - - ))} - - - - - - -
-
+ + + + + + + + + ); } @@ -156,15 +281,46 @@ const useRowStyles = makeStyles({ borderBottom: "unset", }, }, + actionCell: { + width: "96px", + }, + activeActionCell: { + display: "flex", + justifyContent: "space-between", + }, }); -function Row(props: { task: PendingTask }) { +interface RowProps { + task: PendingTaskExtended; + isSelected: boolean; + onSelectChange: (checked: boolean) => void; + onDeleteClick: () => void; + onArchiveClick: () => void; + allActionPending: boolean; + showActions: boolean; + onActionCellEnter: () => void; + onActionCellLeave: () => void; +} + +function Row(props: RowProps) { const { task } = props; const [open, setOpen] = React.useState(false); const classes = useRowStyles(); return ( - + + + ) => + props.onSelectChange(event.target.checked) + } + checked={props.isSelected} + /> + {task.type} + + {props.showActions ? ( + + + + + + + + + + + + + ) : ( + + + + )} + diff --git a/ui/src/reducers/queuesReducer.ts b/ui/src/reducers/queuesReducer.ts index bf29f27..10c4ec8 100644 --- a/ui/src/reducers/queuesReducer.ts +++ b/ui/src/reducers/queuesReducer.ts @@ -44,6 +44,12 @@ import { RUN_RETRY_TASK_SUCCESS, RUN_SCHEDULED_TASK_SUCCESS, TasksActionTypes, + ARCHIVE_PENDING_TASK_SUCCESS, + DELETE_PENDING_TASK_SUCCESS, + BATCH_ARCHIVE_PENDING_TASKS_SUCCESS, + BATCH_DELETE_PENDING_TASKS_SUCCESS, + ARCHIVE_ALL_PENDING_TASKS_SUCCESS, + DELETE_ALL_PENDING_TASKS_SUCCESS, } from "../actions/tasksActions"; import { Queue } from "../api"; @@ -217,6 +223,23 @@ function queuesReducer( return { ...state, data: newData }; } + case ARCHIVE_PENDING_TASK_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + archived: queueInfo.currentStats.archived + 1, + pending: queueInfo.currentStats.pending - 1, + }, + }; + }); + return { ...state, data: newData }; + } + case ARCHIVE_SCHEDULED_TASK_SUCCESS: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { @@ -251,6 +274,22 @@ function queuesReducer( return { ...state, data: newData }; } + case DELETE_PENDING_TASK_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + pending: queueInfo.currentStats.pending - 1, + }, + }; + }); + return { ...state, data: newData }; + } + case DELETE_SCHEDULED_TASK_SUCCESS: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { @@ -267,6 +306,79 @@ function queuesReducer( return { ...state, data: newData }; } + case BATCH_ARCHIVE_PENDING_TASKS_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + archived: + queueInfo.currentStats.archived + + action.payload.archived_keys.length, + pending: + queueInfo.currentStats.pending - + action.payload.archived_keys.length, + }, + }; + }); + return { ...state, data: newData }; + } + + case BATCH_DELETE_PENDING_TASKS_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + pending: + queueInfo.currentStats.pending - + action.payload.deleted_keys.length, + }, + }; + }); + return { ...state, data: newData }; + } + + case ARCHIVE_ALL_PENDING_TASKS_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + archived: + queueInfo.currentStats.archived + queueInfo.currentStats.pending, + pending: 0, + }, + }; + }); + return { ...state, data: newData }; + } + + case DELETE_ALL_PENDING_TASKS_SUCCESS: { + const newData = state.data.map((queueInfo) => { + if (queueInfo.name !== action.queue) { + return queueInfo; + } + return { + ...queueInfo, + currentStats: { + ...queueInfo.currentStats, + pending: 0, + }, + }; + }); + return { ...state, data: newData }; + } + case BATCH_RUN_SCHEDULED_TASKS_SUCCESS: { const newData = state.data.map((queueInfo) => { if (queueInfo.name !== action.queue) { diff --git a/ui/src/reducers/tasksReducer.ts b/ui/src/reducers/tasksReducer.ts index 78a7e8a..eee11fd 100644 --- a/ui/src/reducers/tasksReducer.ts +++ b/ui/src/reducers/tasksReducer.ts @@ -133,6 +133,12 @@ export interface ActiveTaskExtended extends ActiveTask { canceling: boolean; } +export interface PendingTaskExtended extends PendingTask { + // Indicates that a request has been sent for this + // task and awaiting for a response. + requestPending: boolean; +} + export interface ScheduledTaskExtended extends ScheduledTask { // Indicates that a request has been sent for this // task and awaiting for a response. @@ -164,7 +170,7 @@ interface TasksState { batchActionPending: boolean; allActionPending: boolean; error: string; - data: PendingTask[]; + data: PendingTaskExtended[]; }; scheduledTasks: { loading: boolean; @@ -284,7 +290,10 @@ function tasksReducer( ...state.pendingTasks, loading: false, error: "", - data: action.payload.tasks, + data: action.payload.tasks.map((task) => ({ + ...task, + requestPending: false, + })), }, };