Add delete and archive action buttons to PendingTasksTable

This commit is contained in:
Ken Hibino 2021-01-21 16:15:24 -08:00
parent 15bd0a142e
commit caae17a230
3 changed files with 360 additions and 47 deletions

View File

@ -1,4 +1,5 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import clsx from "clsx";
import { connect, ConnectedProps } from "react-redux"; import { connect, ConnectedProps } from "react-redux";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table"; 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 Tooltip from "@material-ui/core/Tooltip";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Box from "@material-ui/core/Box"; import Box from "@material-ui/core/Box";
import Checkbox from "@material-ui/core/Checkbox";
import Collapse from "@material-ui/core/Collapse"; import Collapse from "@material-ui/core/Collapse";
import IconButton from "@material-ui/core/IconButton"; import IconButton from "@material-ui/core/IconButton";
import KeyboardArrowUpIcon from "@material-ui/icons/KeyboardArrowUp"; 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 Typography from "@material-ui/core/Typography";
import TableFooter from "@material-ui/core/TableFooter"; import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination"; 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 SyntaxHighlighter from "./SyntaxHighlighter";
import TablePaginationActions, { import TablePaginationActions, {
defaultPageSize, defaultPageSize,
rowsPerPageOptions, rowsPerPageOptions,
} from "./TablePaginationActions"; } 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 { AppState } from "../store";
import { PendingTask } from "../api";
import { usePolling } from "../hooks"; import { usePolling } from "../hooks";
import { uuidPrefix } from "../utils"; import { uuidPrefix } from "../utils";
import { TableColumn } from "../types/table"; import { TableColumn } from "../types/table";
import { PendingTaskExtended } from "../reducers/tasksReducer";
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
table: { table: {
@ -44,11 +58,21 @@ function mapStateToProps(state: AppState) {
return { return {
loading: state.tasks.pendingTasks.loading, loading: state.tasks.pendingTasks.loading,
tasks: state.tasks.pendingTasks.data, tasks: state.tasks.pendingTasks.data,
batchActionPending: state.tasks.pendingTasks.batchActionPending,
allActionPending: state.tasks.pendingTasks.allActionPending,
pollInterval: state.settings.pollInterval, pollInterval: state.settings.pollInterval,
}; };
} }
const mapDispatchToProps = { listPendingTasksAsync }; const mapDispatchToProps = {
listPendingTasksAsync,
deletePendingTaskAsync,
batchDeletePendingTasksAsync,
deleteAllPendingTasksAsync,
archivePendingTaskAsync,
batchArchivePendingTasksAsync,
archiveAllPendingTasksAsync,
};
const connector = connect(mapStateToProps, mapDispatchToProps); const connector = connect(mapStateToProps, mapDispatchToProps);
@ -64,6 +88,8 @@ function PendingTasksTable(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 [selectedKeys, setSelectedKeys] = useState<string[]>([]);
const [activeTaskId, setActiveTaskId] = useState<string>("");
const handleChangePage = ( const handleChangePage = (
event: React.MouseEvent<HTMLButtonElement> | null, event: React.MouseEvent<HTMLButtonElement> | null,
@ -79,6 +105,35 @@ function PendingTasksTable(props: Props & ReduxProps) {
setPage(0); setPage(0);
}; };
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
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 fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize }; const pageOpts = { page: page + 1, size: pageSize };
listPendingTasksAsync(queue, pageOpts); listPendingTasksAsync(queue, pageOpts);
@ -99,9 +154,42 @@ function PendingTasksTable(props: Props & ReduxProps) {
{ key: "icon", label: "", align: "left" }, { key: "icon", label: "", align: "left" },
{ key: "id", label: "ID", align: "left" }, { key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" }, { key: "type", label: "Type", align: "left" },
{ key: "actions", label: "Actions", align: "center" },
]; ];
const rowCount = props.tasks.length;
const numSelected = selectedKeys.length;
return ( return (
<div>
<TableActions
showIconButtons={numSelected > 0}
iconButtonActions={[
{
tooltip: "Delete",
icon: <DeleteIcon />,
onClick: handleBatchDeleteClick,
disabled: props.batchActionPending,
},
{
tooltip: "Archive",
icon: <ArchiveIcon />,
onClick: handleBatchArchiveClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Delete All",
onClick: handleDeleteAllClick,
disabled: props.allActionPending,
},
{
label: "Archive All",
onClick: handleArchiveAllClick,
disabled: props.allActionPending,
},
]}
/>
<TableContainer component={Paper}> <TableContainer component={Paper}>
<Table <Table
stickyHeader={true} stickyHeader={true}
@ -111,6 +199,19 @@ function PendingTasksTable(props: Props & ReduxProps) {
> >
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
</TableCell>
{columns.map((col) => ( {columns.map((col) => (
<TableCell <TableCell
key={col.key} key={col.key}
@ -124,14 +225,37 @@ function PendingTasksTable(props: Props & ReduxProps) {
</TableHead> </TableHead>
<TableBody> <TableBody>
{props.tasks.map((task) => ( {props.tasks.map((task) => (
<Row key={task.id} task={task} /> <Row
key={task.id}
task={task}
isSelected={selectedKeys.includes(task.key)}
onSelectChange={(checked: boolean) => {
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}
/>
))} ))}
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow> <TableRow>
<TablePagination <TablePagination
rowsPerPageOptions={rowsPerPageOptions} rowsPerPageOptions={rowsPerPageOptions}
colSpan={columns.length} colSpan={columns.length + 1}
count={props.totalTaskCount} count={props.totalTaskCount}
rowsPerPage={pageSize} rowsPerPage={pageSize}
page={page} page={page}
@ -147,6 +271,7 @@ function PendingTasksTable(props: Props & ReduxProps) {
</TableFooter> </TableFooter>
</Table> </Table>
</TableContainer> </TableContainer>
</div>
); );
} }
@ -156,15 +281,46 @@ const useRowStyles = makeStyles({
borderBottom: "unset", 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 { task } = props;
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const classes = useRowStyles(); const classes = useRowStyles();
return ( return (
<React.Fragment> <React.Fragment>
<TableRow key={task.id} className={classes.root}> <TableRow
key={task.id}
className={classes.root}
selected={props.isSelected}
>
<TableCell padding="checkbox">
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
</TableCell>
<TableCell> <TableCell>
<Tooltip title={open ? "Hide Details" : "Show Details"}> <Tooltip title={open ? "Hide Details" : "Show Details"}>
<IconButton <IconButton
@ -180,6 +336,42 @@ function Row(props: { task: PendingTask }) {
{uuidPrefix(task.id)} {uuidPrefix(task.id)}
</TableCell> </TableCell>
<TableCell>{task.type}</TableCell> <TableCell>{task.type}</TableCell>
<TableCell
align="center"
className={clsx(
classes.actionCell,
props.showActions && classes.activeActionCell
)}
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Delete">
<IconButton
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Archive">
<IconButton
onClick={props.onArchiveClick}
disabled={task.requestPending || props.allActionPending}
size="small"
>
<ArchiveIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
</TableRow> </TableRow>
<TableRow> <TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={8}> <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={8}>

View File

@ -44,6 +44,12 @@ import {
RUN_RETRY_TASK_SUCCESS, RUN_RETRY_TASK_SUCCESS,
RUN_SCHEDULED_TASK_SUCCESS, RUN_SCHEDULED_TASK_SUCCESS,
TasksActionTypes, 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"; } from "../actions/tasksActions";
import { Queue } from "../api"; import { Queue } from "../api";
@ -217,6 +223,23 @@ function queuesReducer(
return { ...state, data: newData }; 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: { case ARCHIVE_SCHEDULED_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => { const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) { if (queueInfo.name !== action.queue) {
@ -251,6 +274,22 @@ function queuesReducer(
return { ...state, data: newData }; 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: { case DELETE_SCHEDULED_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => { const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) { if (queueInfo.name !== action.queue) {
@ -267,6 +306,79 @@ function queuesReducer(
return { ...state, data: newData }; 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: { case BATCH_RUN_SCHEDULED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => { const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) { if (queueInfo.name !== action.queue) {

View File

@ -133,6 +133,12 @@ export interface ActiveTaskExtended extends ActiveTask {
canceling: boolean; 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 { export interface ScheduledTaskExtended extends ScheduledTask {
// Indicates that a request has been sent for this // Indicates that a request has been sent for this
// task and awaiting for a response. // task and awaiting for a response.
@ -164,7 +170,7 @@ interface TasksState {
batchActionPending: boolean; batchActionPending: boolean;
allActionPending: boolean; allActionPending: boolean;
error: string; error: string;
data: PendingTask[]; data: PendingTaskExtended[];
}; };
scheduledTasks: { scheduledTasks: {
loading: boolean; loading: boolean;
@ -284,7 +290,10 @@ function tasksReducer(
...state.pendingTasks, ...state.pendingTasks,
loading: false, loading: false,
error: "", error: "",
data: action.payload.tasks, data: action.payload.tasks.map((task) => ({
...task,
requestPending: false,
})),
}, },
}; };