From 97d969171f30e3ddcf4ee2fa338a900cdeb1af3e Mon Sep 17 00:00:00 2001 From: Ken Hibino Date: Sat, 2 Apr 2022 20:40:49 -0700 Subject: [PATCH] Create generic TasksTable component --- ui/src/components/ScheduledTasksTable.tsx | 461 +--------------------- ui/src/components/TasksTable.tsx | 450 +++++++++++++++++++++ ui/src/types/taskState.ts | 8 + 3 files changed, 479 insertions(+), 440 deletions(-) create mode 100644 ui/src/components/TasksTable.tsx create mode 100644 ui/src/types/taskState.ts diff --git a/ui/src/components/ScheduledTasksTable.tsx b/ui/src/components/ScheduledTasksTable.tsx index db4cb01..5a04d70 100644 --- a/ui/src/components/ScheduledTasksTable.tsx +++ b/ui/src/components/ScheduledTasksTable.tsx @@ -1,26 +1,6 @@ -import React, { useState, useCallback } from "react"; -import { useHistory } from "react-router-dom"; +import React from "react"; import { connect, ConnectedProps } from "react-redux"; -import { makeStyles } from "@material-ui/core/styles"; -import Table from "@material-ui/core/Table"; -import TableBody from "@material-ui/core/TableBody"; -import TableCell from "@material-ui/core/TableCell"; -import TableContainer from "@material-ui/core/TableContainer"; -import TableHead from "@material-ui/core/TableHead"; -import TableRow from "@material-ui/core/TableRow"; -import TableFooter from "@material-ui/core/TableFooter"; -import TablePagination from "@material-ui/core/TablePagination"; -import Paper from "@material-ui/core/Paper"; -import Tooltip from "@material-ui/core/Tooltip"; -import Checkbox from "@material-ui/core/Checkbox"; -import IconButton from "@material-ui/core/IconButton"; -import PlayArrowIcon from "@material-ui/icons/PlayArrow"; -import DeleteIcon from "@material-ui/icons/Delete"; -import ArchiveIcon from "@material-ui/icons/Archive"; -import MoreHorizIcon from "@material-ui/icons/MoreHoriz"; -import Alert from "@material-ui/lab/Alert"; -import AlertTitle from "@material-ui/lab/AlertTitle"; -import SyntaxHighlighter from "./SyntaxHighlighter"; +import TasksTable from "./TasksTable"; import { batchDeleteScheduledTasksAsync, batchRunScheduledTasksAsync, @@ -35,32 +15,7 @@ import { } from "../actions/tasksActions"; import { taskRowsPerPageChange } from "../actions/settingsActions"; import { AppState } from "../store"; -import TablePaginationActions, { - rowsPerPageOptions, -} from "./TablePaginationActions"; -import TableActions from "./TableActions"; -import { durationBefore, uuidPrefix, prettifyPayload } from "../utils"; -import { usePolling } from "../hooks"; -import { TaskInfoExtended } from "../reducers/tasksReducer"; import { TableColumn } from "../types/table"; -import { taskDetailsPath } from "../paths"; -import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; - -const useStyles = makeStyles((theme) => ({ - table: { - minWidth: 650, - }, - stickyHeaderCell: { - background: theme.palette.background.paper, - }, - alert: { - borderTopLeftRadius: 0, - borderTopRightRadius: 0, - }, - pagination: { - border: "none", - }, -})); function mapStateToProps(state: AppState) { return { @@ -75,16 +30,16 @@ function mapStateToProps(state: AppState) { } const mapDispatchToProps = { - listScheduledTasksAsync, - batchDeleteScheduledTasksAsync, - batchRunScheduledTasksAsync, - batchArchiveScheduledTasksAsync, - deleteAllScheduledTasksAsync, - runAllScheduledTasksAsync, - archiveAllScheduledTasksAsync, - deleteScheduledTaskAsync, - runScheduledTaskAsync, - archiveScheduledTaskAsync, + listTasks: listScheduledTasksAsync, + batchDeleteTasks: batchDeleteScheduledTasksAsync, + batchRunTasks: batchRunScheduledTasksAsync, + batchArchiveTasks: batchArchiveScheduledTasksAsync, + deleteAllTasks: deleteAllScheduledTasksAsync, + runAllTasks: runAllScheduledTasksAsync, + archiveAllTasks: archiveAllScheduledTasksAsync, + deleteTask: deleteScheduledTaskAsync, + runTask: runScheduledTaskAsync, + archiveTask: archiveScheduledTaskAsync, taskRowsPerPageChange, }; @@ -97,390 +52,16 @@ interface Props { totalTaskCount: number; // totoal number of scheduled tasks. } +const columns: TableColumn[] = [ + { key: "id", label: "ID", align: "left" }, + { key: "type", label: "Type", align: "left" }, + { key: "payload", label: "Payload", align: "left" }, + { key: "process_in", label: "Process In", align: "left" }, + { key: "actions", label: "Actions", align: "center" }, +]; + function ScheduledTasksTable(props: Props & ReduxProps) { - const { pollInterval, listScheduledTasksAsync, queue, pageSize } = props; - const classes = useStyles(); - const [page, setPage] = useState(0); - const [selectedIds, setSelectedIds] = useState([]); - const [activeTaskId, setActiveTaskId] = useState(""); - - const handlePageChange = ( - event: React.MouseEvent | null, - newPage: number - ) => { - setPage(newPage); - }; - - const handleRowsPerPageChange = ( - event: React.ChangeEvent - ) => { - props.taskRowsPerPageChange(parseInt(event.target.value, 10)); - setPage(0); - }; - - const handleSelectAllClick = (event: React.ChangeEvent) => { - if (event.target.checked) { - const newSelected = props.tasks.map((t) => t.id); - setSelectedIds(newSelected); - } else { - setSelectedIds([]); - } - }; - - const handleRunAllClick = () => { - props.runAllScheduledTasksAsync(queue); - }; - - const handleDeleteAllClick = () => { - props.deleteAllScheduledTasksAsync(queue); - }; - - const handleArchiveAllClick = () => { - props.archiveAllScheduledTasksAsync(queue); - }; - - const handleBatchRunClick = () => { - props - .batchRunScheduledTasksAsync(queue, selectedIds) - .then(() => setSelectedIds([])); - }; - - const handleBatchDeleteClick = () => { - props - .batchDeleteScheduledTasksAsync(queue, selectedIds) - .then(() => setSelectedIds([])); - }; - - const handleBatchArchiveClick = () => { - props - .batchArchiveScheduledTasksAsync(queue, selectedIds) - .then(() => setSelectedIds([])); - }; - - const fetchData = useCallback(() => { - const pageOpts = { page: page + 1, size: pageSize }; - listScheduledTasksAsync(queue, pageOpts); - }, [page, pageSize, queue, listScheduledTasksAsync]); - - usePolling(fetchData, pollInterval); - - if (props.error.length > 0) { - return ( - - Error - {props.error} - - ); - } - if (props.tasks.length === 0) { - return ( - - Info - No scheduled tasks at this time. - - ); - } - - const columns: TableColumn[] = [ - { key: "id", label: "ID", align: "left" }, - { key: "type", label: "Type", align: "left" }, - { key: "payload", label: "Payload", align: "left" }, - { key: "process_in", label: "Process In", align: "left" }, - { key: "actions", label: "Actions", align: "center" }, - ]; - - const rowCount = props.tasks.length; - const numSelected = selectedIds.length; - return ( -
- {!window.READ_ONLY && ( - 0} - iconButtonActions={[ - { - tooltip: "Delete", - icon: , - onClick: handleBatchDeleteClick, - disabled: props.batchActionPending, - }, - { - tooltip: "Archive", - icon: , - onClick: handleBatchArchiveClick, - disabled: props.batchActionPending, - }, - { - tooltip: "Run", - icon: , - onClick: handleBatchRunClick, - disabled: props.batchActionPending, - }, - ]} - menuItemActions={[ - { - label: "Delete All", - onClick: handleDeleteAllClick, - disabled: props.allActionPending, - }, - { - label: "Archive All", - onClick: handleArchiveAllClick, - disabled: props.allActionPending, - }, - { - label: "Run All", - onClick: handleRunAllClick, - disabled: props.allActionPending, - }, - ]} - /> - )} - - - - - {!window.READ_ONLY && ( - - - 0 && numSelected < rowCount} - checked={rowCount > 0 && numSelected === rowCount} - onChange={handleSelectAllClick} - inputProps={{ - "aria-label": "select all tasks shown in the table", - }} - /> - - - )} - {columns - .filter((col) => { - // Filter out actions column in readonly mode. - return !window.READ_ONLY || col.key !== "actions"; - }) - .map((col) => ( - - {col.label} - - ))} - - - - {props.tasks.map((task) => ( - { - if (checked) { - setSelectedIds(selectedIds.concat(task.id)); - } else { - setSelectedIds(selectedIds.filter((id) => id !== task.id)); - } - }} - onRunClick={() => { - props.runScheduledTaskAsync(queue, task.id); - }} - onDeleteClick={() => { - props.deleteScheduledTaskAsync(queue, task.id); - }} - onArchiveClick={() => { - props.archiveScheduledTaskAsync(queue, task.id); - }} - onActionCellEnter={() => setActiveTaskId(task.id)} - onActionCellLeave={() => setActiveTaskId("")} - showActions={activeTaskId === task.id} - /> - ))} - - - - - - -
-
-
- ); + return ; } -const useRowStyles = makeStyles((theme) => ({ - root: { - cursor: "pointer", - "& #copy-button": { - display: "none", - }, - "&:hover": { - boxShadow: theme.shadows[2], - "& #copy-button": { - display: "inline-block", - }, - }, - "&:hover $copyButton": { - display: "inline-block", - }, - "&:hover .MuiTableCell-root": { - borderBottomColor: theme.palette.background.paper, - }, - }, - actionCell: { - width: "140px", - }, - actionButton: { - marginLeft: 3, - marginRight: 3, - }, - idCell: { - width: "200px", - }, - copyButton: { - display: "none", - }, - IdGroup: { - display: "flex", - alignItems: "center", - }, -})); - -interface RowProps { - task: TaskInfoExtended; - isSelected: boolean; - onSelectChange: (checked: boolean) => void; - onRunClick: () => void; - onDeleteClick: () => void; - onArchiveClick: () => void; - allActionPending: boolean; - showActions: boolean; - onActionCellEnter: () => void; - onActionCellLeave: () => void; -} - -function Row(props: RowProps) { - const { task } = props; - const classes = useRowStyles(); - const history = useHistory(); - return ( - history.push(taskDetailsPath(task.queue, task.id))} - > - {!window.READ_ONLY && ( - e.stopPropagation()}> - - ) => - props.onSelectChange(event.target.checked) - } - checked={props.isSelected} - /> - - - )} - -
- {uuidPrefix(task.id)} - - { - e.stopPropagation(); - navigator.clipboard.writeText(task.id); - }} - size="small" - className={classes.copyButton} - > - - - -
-
- {task.type} - - - {prettifyPayload(task.payload)} - - - {durationBefore(task.next_process_at)} - {!window.READ_ONLY && ( - e.stopPropagation()} - > - {props.showActions ? ( - - - - - - - - - - - - - - - - - - ) : ( - - - - )} - - )} -
- ); -} export default connector(ScheduledTasksTable); diff --git a/ui/src/components/TasksTable.tsx b/ui/src/components/TasksTable.tsx new file mode 100644 index 0000000..6644476 --- /dev/null +++ b/ui/src/components/TasksTable.tsx @@ -0,0 +1,450 @@ +import React, { useState, useCallback } from "react"; +import { useHistory } from "react-router-dom"; +import { makeStyles } from "@material-ui/core/styles"; +import Table from "@material-ui/core/Table"; +import TableBody from "@material-ui/core/TableBody"; +import TableCell from "@material-ui/core/TableCell"; +import TableContainer from "@material-ui/core/TableContainer"; +import TableHead from "@material-ui/core/TableHead"; +import TableRow from "@material-ui/core/TableRow"; +import TableFooter from "@material-ui/core/TableFooter"; +import TablePagination from "@material-ui/core/TablePagination"; +import Paper from "@material-ui/core/Paper"; +import Tooltip from "@material-ui/core/Tooltip"; +import Checkbox from "@material-ui/core/Checkbox"; +import IconButton from "@material-ui/core/IconButton"; +import PlayArrowIcon from "@material-ui/icons/PlayArrow"; +import DeleteIcon from "@material-ui/icons/Delete"; +import ArchiveIcon from "@material-ui/icons/Archive"; +import MoreHorizIcon from "@material-ui/icons/MoreHoriz"; +import Alert from "@material-ui/lab/Alert"; +import AlertTitle from "@material-ui/lab/AlertTitle"; +import SyntaxHighlighter from "./SyntaxHighlighter"; +import TablePaginationActions, { + rowsPerPageOptions, +} from "./TablePaginationActions"; +import TableActions from "./TableActions"; +import { durationBefore, uuidPrefix, prettifyPayload } from "../utils"; +import { usePolling } from "../hooks"; +import { TaskInfoExtended } from "../reducers/tasksReducer"; +import { TableColumn } from "../types/table"; +import { taskDetailsPath } from "../paths"; +import FileCopyOutlinedIcon from "@material-ui/icons/FileCopyOutlined"; +import { PaginationOptions } from "../api"; +import { TaskState } from "../types/taskState"; + +const useStyles = makeStyles((theme) => ({ + table: { + minWidth: 650, + }, + stickyHeaderCell: { + background: theme.palette.background.paper, + }, + alert: { + borderTopLeftRadius: 0, + borderTopRightRadius: 0, + }, + pagination: { + border: "none", + }, +})); + +interface Props { + queue: string; // name of the queue. + totalTaskCount: number; // totoal number of tasks in the given state. + taskState: TaskState; + loading: boolean; + error: string; + tasks: TaskInfoExtended[]; + batchActionPending: boolean; + allActionPending: boolean; + pollInterval: number; + pageSize: number; + columns: TableColumn[]; + + // actions + listTasks: (qname: string, pgn: PaginationOptions) => void; + batchDeleteTasks: (qname: string, taskIds: string[]) => Promise; + batchRunTasks: (qname: string, taskIds: string[]) => Promise; + batchArchiveTasks: (qname: string, taskIds: string[]) => Promise; + deleteAllTasks: (qname: string) => Promise; + runAllTasks: (qname: string) => Promise; + archiveAllTasks: (qname: string) => Promise; + deleteTask: (qname: string, taskId: string) => Promise; + runTask: (qname: string, taskId: string) => Promise; + archiveTask: (qname: string, taskId: string) => Promise; + taskRowsPerPageChange: (n: number) => void; +} + +export default function TasksTable(props: Props) { + const { pollInterval, listTasks, queue, pageSize } = props; + const classes = useStyles(); + const [page, setPage] = useState(0); + const [selectedIds, setSelectedIds] = useState([]); + const [activeTaskId, setActiveTaskId] = useState(""); + + const handlePageChange = ( + event: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + event: React.ChangeEvent + ) => { + props.taskRowsPerPageChange(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleSelectAllClick = (event: React.ChangeEvent) => { + if (event.target.checked) { + const newSelected = props.tasks.map((t) => t.id); + setSelectedIds(newSelected); + } else { + setSelectedIds([]); + } + }; + + const handleRunAllClick = () => { + props.runAllTasks(queue); + }; + + const handleDeleteAllClick = () => { + props.deleteAllTasks(queue); + }; + + const handleArchiveAllClick = () => { + props.archiveAllTasks(queue); + }; + + const handleBatchRunClick = () => { + props.batchRunTasks(queue, selectedIds).then(() => setSelectedIds([])); + }; + + const handleBatchDeleteClick = () => { + props.batchDeleteTasks(queue, selectedIds).then(() => setSelectedIds([])); + }; + + const handleBatchArchiveClick = () => { + props.batchArchiveTasks(queue, selectedIds).then(() => setSelectedIds([])); + }; + + const fetchData = useCallback(() => { + const pageOpts = { page: page + 1, size: pageSize }; + listTasks(queue, pageOpts); + }, [page, pageSize, queue, listTasks]); + + usePolling(fetchData, pollInterval); + + if (props.error.length > 0) { + return ( + + Error + {props.error} + + ); + } + if (props.tasks.length === 0) { + return ( + + Info + No {props.taskState} tasks at this time. + + ); + } + + const rowCount = props.tasks.length; + const numSelected = selectedIds.length; + return ( +
+ {!window.READ_ONLY && ( + 0} + iconButtonActions={[ + { + tooltip: "Delete", + icon: , + onClick: handleBatchDeleteClick, + disabled: props.batchActionPending, + }, + { + tooltip: "Archive", + icon: , + onClick: handleBatchArchiveClick, + disabled: props.batchActionPending, + }, + { + tooltip: "Run", + icon: , + onClick: handleBatchRunClick, + disabled: props.batchActionPending, + }, + ]} + menuItemActions={[ + { + label: "Delete All", + onClick: handleDeleteAllClick, + disabled: props.allActionPending, + }, + { + label: "Archive All", + onClick: handleArchiveAllClick, + disabled: props.allActionPending, + }, + { + label: "Run All", + onClick: handleRunAllClick, + disabled: props.allActionPending, + }, + ]} + /> + )} + + + + + {!window.READ_ONLY && ( + + + 0 && numSelected < rowCount} + checked={rowCount > 0 && numSelected === rowCount} + onChange={handleSelectAllClick} + inputProps={{ + "aria-label": "select all tasks shown in the table", + }} + /> + + + )} + {props.columns + .filter((col) => { + // Filter out actions column in readonly mode. + return !window.READ_ONLY || col.key !== "actions"; + }) + .map((col) => ( + + {col.label} + + ))} + + + + {props.tasks.map((task) => ( + { + if (checked) { + setSelectedIds(selectedIds.concat(task.id)); + } else { + setSelectedIds(selectedIds.filter((id) => id !== task.id)); + } + }} + onRunClick={() => { + props.runTask(queue, task.id); + }} + onDeleteClick={() => { + props.deleteTask(queue, task.id); + }} + onArchiveClick={() => { + props.archiveTask(queue, task.id); + }} + onActionCellEnter={() => setActiveTaskId(task.id)} + onActionCellLeave={() => setActiveTaskId("")} + showActions={activeTaskId === task.id} + /> + ))} + + + + + + +
+
+
+ ); +} + +const useRowStyles = makeStyles((theme) => ({ + root: { + cursor: "pointer", + "& #copy-button": { + display: "none", + }, + "&:hover": { + boxShadow: theme.shadows[2], + "& #copy-button": { + display: "inline-block", + }, + }, + "&:hover $copyButton": { + display: "inline-block", + }, + "&:hover .MuiTableCell-root": { + borderBottomColor: theme.palette.background.paper, + }, + }, + actionCell: { + width: "140px", + }, + actionButton: { + marginLeft: 3, + marginRight: 3, + }, + idCell: { + width: "200px", + }, + copyButton: { + display: "none", + }, + IdGroup: { + display: "flex", + alignItems: "center", + }, +})); + +interface RowProps { + task: TaskInfoExtended; + isSelected: boolean; + onSelectChange: (checked: boolean) => void; + onRunClick: () => void; + onDeleteClick: () => void; + onArchiveClick: () => void; + allActionPending: boolean; + showActions: boolean; + onActionCellEnter: () => void; + onActionCellLeave: () => void; +} + +function Row(props: RowProps) { + const { task } = props; + const classes = useRowStyles(); + const history = useHistory(); + return ( + history.push(taskDetailsPath(task.queue, task.id))} + > + {!window.READ_ONLY && ( + e.stopPropagation()}> + + ) => + props.onSelectChange(event.target.checked) + } + checked={props.isSelected} + /> + + + )} + +
+ {uuidPrefix(task.id)} + + { + e.stopPropagation(); + navigator.clipboard.writeText(task.id); + }} + size="small" + className={classes.copyButton} + > + + + +
+
+ {task.type} + + + {prettifyPayload(task.payload)} + + + {durationBefore(task.next_process_at)} + {!window.READ_ONLY && ( + e.stopPropagation()} + > + {props.showActions ? ( + + + + + + + + + + + + + + + + + + ) : ( + + + + )} + + )} +
+ ); +} diff --git a/ui/src/types/taskState.ts b/ui/src/types/taskState.ts new file mode 100644 index 0000000..b4c9185 --- /dev/null +++ b/ui/src/types/taskState.ts @@ -0,0 +1,8 @@ +export type TaskState = + | "active" + | "pending" + | "aggregating" + | "scheduled" + | "retry" + | "archived" + | "completed";