Add completed state

This commit is contained in:
Ken Hibino
2021-11-06 15:23:10 -07:00
committed by GitHub
parent ddb1798ce8
commit 741a3c59fa
36 changed files with 1234 additions and 201 deletions

View File

@@ -4,6 +4,7 @@ import {
batchDeleteArchivedTasks,
batchDeleteRetryTasks,
batchDeleteScheduledTasks,
batchDeleteCompletedTasks,
BatchDeleteTasksResponse,
batchArchiveRetryTasks,
batchArchiveScheduledTasks,
@@ -17,23 +18,22 @@ import {
deleteAllArchivedTasks,
deleteAllRetryTasks,
deleteAllScheduledTasks,
deleteAllCompletedTasks,
deleteArchivedTask,
deleteRetryTask,
deleteScheduledTask,
deleteCompletedTask,
archiveAllRetryTasks,
archiveAllScheduledTasks,
archiveRetryTask,
archiveScheduledTask,
listActiveTasks,
ListActiveTasksResponse,
listArchivedTasks,
ListArchivedTasksResponse,
listPendingTasks,
ListPendingTasksResponse,
ListTasksResponse,
listRetryTasks,
ListRetryTasksResponse,
listScheduledTasks,
ListScheduledTasksResponse,
listCompletedTasks,
PaginationOptions,
runAllArchivedTasks,
runAllRetryTasks,
@@ -72,6 +72,9 @@ export const LIST_RETRY_TASKS_ERROR = "LIST_RETRY_TASKS_ERROR";
export const LIST_ARCHIVED_TASKS_BEGIN = "LIST_ARCHIVED_TASKS_BEGIN";
export const LIST_ARCHIVED_TASKS_SUCCESS = "LIST_ARCHIVED_TASKS_SUCCESS";
export const LIST_ARCHIVED_TASKS_ERROR = "LIST_ARCHIVED_TASKS_ERROR";
export const LIST_COMPLETED_TASKS_BEGIN = "LIST_COMPLETED_TASKS_BEGIN";
export const LIST_COMPLETED_TASKS_SUCCESS = "LIST_COMPLETED_TASKS_SUCCESS";
export const LIST_COMPLETED_TASKS_ERROR = "LIST_COMPLETED_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";
@@ -213,6 +216,21 @@ export const DELETE_ALL_ARCHIVED_TASKS_SUCCESS =
"DELETE_ALL_ARCHIVED_TASKS_SUCCESS";
export const DELETE_ALL_ARCHIVED_TASKS_ERROR =
"DELETE_ALL_ARCHIVED_TASKS_ERROR";
export const DELETE_COMPLETED_TASK_BEGIN = "DELETE_COMPLETED_TASK_BEGIN";
export const DELETE_COMPLETED_TASK_SUCCESS = "DELETE_COMPLETED_TASK_SUCCESS";
export const DELETE_COMPLETED_TASK_ERROR = "DELETE_COMPLETED_TASK_ERROR";
export const DELETE_ALL_COMPLETED_TASKS_BEGIN =
"DELETE_ALL_COMPLETED_TASKS_BEGIN";
export const DELETE_ALL_COMPLETED_TASKS_SUCCESS =
"DELETE_ALL_COMPLETED_TASKS_SUCCESS";
export const DELETE_ALL_COMPLETED_TASKS_ERROR =
"DELETE_ALL_COMPLETED_TASKS_ERROR";
export const BATCH_DELETE_COMPLETED_TASKS_BEGIN =
"BATCH_DELETE_COMPLETED_TASKS_BEGIN";
export const BATCH_DELETE_COMPLETED_TASKS_SUCCESS =
"BATCH_DELETE_COMPLETED_TASKS_SUCCESS";
export const BATCH_DELETE_COMPLETED_TASKS_ERROR =
"BATCH_DELETE_COMPLETED_TASKS_ERROR";
interface GetTaskInfoBeginAction {
type: typeof GET_TASK_INFO_BEGIN;
@@ -236,7 +254,7 @@ interface ListActiveTasksBeginAction {
interface ListActiveTasksSuccessAction {
type: typeof LIST_ACTIVE_TASKS_SUCCESS;
queue: string;
payload: ListActiveTasksResponse;
payload: ListTasksResponse;
}
interface ListActiveTasksErrorAction {
@@ -253,7 +271,7 @@ interface ListPendingTasksBeginAction {
interface ListPendingTasksSuccessAction {
type: typeof LIST_PENDING_TASKS_SUCCESS;
queue: string;
payload: ListPendingTasksResponse;
payload: ListTasksResponse;
}
interface ListPendingTasksErrorAction {
@@ -270,7 +288,7 @@ interface ListScheduledTasksBeginAction {
interface ListScheduledTasksSuccessAction {
type: typeof LIST_SCHEDULED_TASKS_SUCCESS;
queue: string;
payload: ListScheduledTasksResponse;
payload: ListTasksResponse;
}
interface ListScheduledTasksErrorAction {
@@ -287,7 +305,7 @@ interface ListRetryTasksBeginAction {
interface ListRetryTasksSuccessAction {
type: typeof LIST_RETRY_TASKS_SUCCESS;
queue: string;
payload: ListRetryTasksResponse;
payload: ListTasksResponse;
}
interface ListRetryTasksErrorAction {
@@ -304,7 +322,7 @@ interface ListArchivedTasksBeginAction {
interface ListArchivedTasksSuccessAction {
type: typeof LIST_ARCHIVED_TASKS_SUCCESS;
queue: string;
payload: ListArchivedTasksResponse;
payload: ListTasksResponse;
}
interface ListArchivedTasksErrorAction {
@@ -313,6 +331,23 @@ interface ListArchivedTasksErrorAction {
error: string; // error description
}
interface ListCompletedTasksBeginAction {
type: typeof LIST_COMPLETED_TASKS_BEGIN;
queue: string;
}
interface ListCompletedTasksSuccessAction {
type: typeof LIST_COMPLETED_TASKS_SUCCESS;
queue: string;
payload: ListTasksResponse;
}
interface ListCompletedTasksErrorAction {
type: typeof LIST_COMPLETED_TASKS_ERROR;
queue: string;
error: string; // error description
}
interface CancelActiveTaskBeginAction {
type: typeof CANCEL_ACTIVE_TASK_BEGIN;
queue: string;
@@ -911,6 +946,61 @@ interface DeleteAllArchivedTasksErrorAction {
error: string;
}
interface DeleteCompletedTaskBeginAction {
type: typeof DELETE_COMPLETED_TASK_BEGIN;
queue: string;
taskId: string;
}
interface DeleteCompletedTaskSuccessAction {
type: typeof DELETE_COMPLETED_TASK_SUCCESS;
queue: string;
taskId: string;
}
interface DeleteCompletedTaskErrorAction {
type: typeof DELETE_COMPLETED_TASK_ERROR;
queue: string;
taskId: string;
error: string;
}
interface BatchDeleteCompletedTasksBeginAction {
type: typeof BATCH_DELETE_COMPLETED_TASKS_BEGIN;
queue: string;
taskIds: string[];
}
interface BatchDeleteCompletedTasksSuccessAction {
type: typeof BATCH_DELETE_COMPLETED_TASKS_SUCCESS;
queue: string;
payload: BatchDeleteTasksResponse;
}
interface BatchDeleteCompletedTasksErrorAction {
type: typeof BATCH_DELETE_COMPLETED_TASKS_ERROR;
queue: string;
taskIds: string[];
error: string;
}
interface DeleteAllCompletedTasksBeginAction {
type: typeof DELETE_ALL_COMPLETED_TASKS_BEGIN;
queue: string;
}
interface DeleteAllCompletedTasksSuccessAction {
type: typeof DELETE_ALL_COMPLETED_TASKS_SUCCESS;
queue: string;
deleted: number;
}
interface DeleteAllCompletedTasksErrorAction {
type: typeof DELETE_ALL_COMPLETED_TASKS_ERROR;
queue: string;
error: string;
}
// Union of all tasks related action types.
export type TasksActionTypes =
| GetTaskInfoBeginAction
@@ -931,6 +1021,9 @@ export type TasksActionTypes =
| ListArchivedTasksBeginAction
| ListArchivedTasksSuccessAction
| ListArchivedTasksErrorAction
| ListCompletedTasksBeginAction
| ListCompletedTasksSuccessAction
| ListCompletedTasksErrorAction
| CancelActiveTaskBeginAction
| CancelActiveTaskSuccessAction
| CancelActiveTaskErrorAction
@@ -1029,7 +1122,16 @@ export type TasksActionTypes =
| RunAllArchivedTasksErrorAction
| DeleteAllArchivedTasksBeginAction
| DeleteAllArchivedTasksSuccessAction
| DeleteAllArchivedTasksErrorAction;
| DeleteAllArchivedTasksErrorAction
| DeleteCompletedTaskBeginAction
| DeleteCompletedTaskSuccessAction
| DeleteCompletedTaskErrorAction
| BatchDeleteCompletedTasksBeginAction
| BatchDeleteCompletedTasksSuccessAction
| BatchDeleteCompletedTasksErrorAction
| DeleteAllCompletedTasksBeginAction
| DeleteAllCompletedTasksSuccessAction
| DeleteAllCompletedTasksErrorAction;
export function getTaskInfoAsync(qname: string, id: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
@@ -1039,15 +1141,15 @@ export function getTaskInfoAsync(qname: string, id: string) {
dispatch({
type: GET_TASK_INFO_SUCCESS,
payload: response,
})
});
} catch (error) {
console.error("getTaskInfoAsync: ", toErrorStringWithHttpStatus(error));
dispatch({
type: GET_TASK_INFO_ERROR,
error: toErrorString(error),
})
});
}
}
};
}
export function listActiveTasksAsync(
@@ -1185,6 +1287,33 @@ export function listArchivedTasksAsync(
};
}
export function listCompletedTasksAsync(
qname: string,
pageOpts?: PaginationOptions
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
try {
dispatch({ type: LIST_COMPLETED_TASKS_BEGIN, queue: qname });
const response = await listCompletedTasks(qname, pageOpts);
dispatch({
type: LIST_COMPLETED_TASKS_SUCCESS,
queue: qname,
payload: response,
});
} catch (error) {
console.error(
"listCompletedTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: LIST_COMPLETED_TASKS_ERROR,
queue: qname,
error: toErrorString(error),
});
}
};
}
export function cancelActiveTaskAsync(queue: string, taskId: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId });
@@ -1395,10 +1524,7 @@ export function deletePendingTaskAsync(queue: string, taskId: string) {
};
}
export function batchDeletePendingTasksAsync(
queue: string,
taskIds: string[]
) {
export function batchDeletePendingTasksAsync(queue: string, taskIds: string[]) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: BATCH_DELETE_PENDING_TASKS_BEGIN, queue, taskIds });
try {
@@ -1938,3 +2064,76 @@ export function runAllArchivedTasksAsync(queue: string) {
}
};
}
export function deleteCompletedTaskAsync(queue: string, taskId: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_COMPLETED_TASK_BEGIN, queue, taskId });
try {
await deleteCompletedTask(queue, taskId);
dispatch({ type: DELETE_COMPLETED_TASK_SUCCESS, queue, taskId });
} catch (error) {
console.error(
"deleteCompletedTaskAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: DELETE_COMPLETED_TASK_ERROR,
error: toErrorString(error),
queue,
taskId,
});
}
};
}
export function batchDeleteCompletedTasksAsync(
queue: string,
taskIds: string[]
) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: BATCH_DELETE_COMPLETED_TASKS_BEGIN, queue, taskIds });
try {
const response = await batchDeleteCompletedTasks(queue, taskIds);
dispatch({
type: BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
queue: queue,
payload: response,
});
} catch (error) {
console.error(
"batchDeleteCompletedTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: BATCH_DELETE_COMPLETED_TASKS_ERROR,
error: toErrorString(error),
queue,
taskIds,
});
}
};
}
export function deleteAllCompletedTasksAsync(queue: string) {
return async (dispatch: Dispatch<TasksActionTypes>) => {
dispatch({ type: DELETE_ALL_COMPLETED_TASKS_BEGIN, queue });
try {
const response = await deleteAllCompletedTasks(queue);
dispatch({
type: DELETE_ALL_COMPLETED_TASKS_SUCCESS,
deleted: response.deleted,
queue,
});
} catch (error) {
console.error(
"deleteAllCompletedTasksAsync: ",
toErrorStringWithHttpStatus(error)
);
dispatch({
type: DELETE_ALL_COMPLETED_TASKS_ERROR,
error: toErrorString(error),
queue,
});
}
};
}

View File

@@ -5,34 +5,16 @@ import queryString from "query-string";
// the static file server.
// In developement, we assume that the API server is listening on port 8080.
const BASE_URL =
process.env.NODE_ENV === "production" ? `${window.ROOT_PATH}/api` : `http://localhost:8080${window.ROOT_PATH}/api`;
process.env.NODE_ENV === "production"
? `${window.ROOT_PATH}/api`
: `http://localhost:8080${window.ROOT_PATH}/api`;
export interface ListQueuesResponse {
queues: Queue[];
}
export interface ListActiveTasksResponse {
tasks: ActiveTask[];
stats: Queue;
}
export interface ListPendingTasksResponse {
tasks: PendingTask[];
stats: Queue;
}
export interface ListScheduledTasksResponse {
tasks: ScheduledTask[];
stats: Queue;
}
export interface ListRetryTasksResponse {
tasks: RetryTask[];
stats: Queue;
}
export interface ListArchivedTasksResponse {
tasks: ArchivedTask[];
export interface ListTasksResponse {
tasks: TaskInfo[];
stats: Queue;
}
@@ -239,6 +221,7 @@ export interface Queue {
scheduled: number;
retry: number;
archived: number;
completed: number;
processed: number;
failed: number;
timestamp: string;
@@ -251,18 +234,13 @@ export interface DailyStat {
failed: number;
}
// BaseTask corresponds to asynq.Task type.
interface BaseTask {
type: string;
payload: string;
}
export interface TaskInfo {
id: string;
queue: string;
type: string;
payload: string;
state: string;
start_time: string; // Only applies to task.state == 'active'
max_retry: number;
retried: number;
last_failed_at: string;
@@ -270,51 +248,9 @@ export interface TaskInfo {
next_process_at: string;
timeout_seconds: number;
deadline: string;
}
export interface ActiveTask extends BaseTask {
id: string;
queue: string;
start_time: string;
deadline: string;
max_retry: number;
retried: number;
error_message: string;
}
export interface PendingTask extends BaseTask {
id: string;
queue: string;
max_retry: number;
retried: number;
error_message: string;
}
export interface ScheduledTask extends BaseTask {
id: string;
queue: string;
max_retry: number;
retried: number;
error_message: string;
next_process_at: string;
}
export interface RetryTask extends BaseTask {
id: string;
queue: string;
next_process_at: string;
max_retry: number;
retried: number;
error_message: string;
}
export interface ArchivedTask extends BaseTask {
id: string;
queue: string;
max_retry: number;
retried: number;
last_failed_at: string;
error_message: string;
completed_at: string;
result: string;
ttl_seconds: number;
}
export interface ServerInfo {
@@ -396,7 +332,10 @@ export async function listQueueStats(): Promise<ListQueueStatsResponse> {
return resp.data;
}
export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo> {
export async function getTaskInfo(
qname: string,
id: string
): Promise<TaskInfo> {
const url = `${BASE_URL}/queues/${qname}/tasks/${id}`;
const resp = await axios({
method: "get",
@@ -408,7 +347,7 @@ export async function getTaskInfo(qname: string, id: string): Promise<TaskInfo>
export async function listActiveTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListActiveTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/active_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@@ -454,7 +393,7 @@ export async function batchCancelActiveTasks(
export async function listPendingTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListPendingTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/pending_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@@ -469,7 +408,7 @@ export async function listPendingTasks(
export async function listScheduledTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListScheduledTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/scheduled_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@@ -484,7 +423,7 @@ export async function listScheduledTasks(
export async function listRetryTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListRetryTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/retry_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@@ -499,7 +438,7 @@ export async function listRetryTasks(
export async function listArchivedTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListArchivedTasksResponse> {
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/archived_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
@@ -511,6 +450,21 @@ export async function listArchivedTasks(
return resp.data;
}
export async function listCompletedTasks(
qname: string,
pageOpts?: PaginationOptions
): Promise<ListTasksResponse> {
let url = `${BASE_URL}/queues/${qname}/completed_tasks`;
if (pageOpts) {
url += `?${queryString.stringify(pageOpts)}`;
}
const resp = await axios({
method: "get",
url,
});
return resp.data;
}
export async function archivePendingTask(
qname: string,
taskId: string
@@ -833,6 +787,40 @@ export async function runAllArchivedTasks(qname: string): Promise<void> {
});
}
export async function deleteCompletedTask(
qname: string,
taskId: string
): Promise<void> {
await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/completed_tasks/${taskId}`,
});
}
export async function batchDeleteCompletedTasks(
qname: string,
taskIds: string[]
): Promise<BatchDeleteTasksResponse> {
const resp = await axios({
method: "post",
url: `${BASE_URL}/queues/${qname}/completed_tasks:batch_delete`,
data: {
task_ids: taskIds,
},
});
return resp.data;
}
export async function deleteAllCompletedTasks(
qname: string
): Promise<DeleteAllTasksResponse> {
const resp = await axios({
method: "delete",
url: `${BASE_URL}/queues/${qname}/completed_tasks:delete_all`,
});
return resp.data;
}
export async function listServers(): Promise<ListServersResponse> {
const resp = await axios({
method: "get",

View File

@@ -37,7 +37,7 @@ import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions";
import { timeAgo, uuidPrefix } from "../utils";
import { usePolling } from "../hooks";
import { ArchivedTaskExtended } from "../reducers/tasksReducer";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
@@ -311,7 +311,7 @@ const useRowStyles = makeStyles((theme) => ({
}));
interface RowProps {
task: ArchivedTaskExtended;
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onRunClick: () => void;

View File

@@ -0,0 +1,376 @@
import React, { useCallback, useState } from "react";
import { useHistory } from "react-router-dom";
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 Checkbox from "@material-ui/core/Checkbox";
import TableContainer from "@material-ui/core/TableContainer";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Tooltip from "@material-ui/core/Tooltip";
import Paper from "@material-ui/core/Paper";
import IconButton from "@material-ui/core/IconButton";
import DeleteIcon from "@material-ui/icons/Delete";
import MoreHorizIcon from "@material-ui/icons/MoreHoriz";
import TableFooter from "@material-ui/core/TableFooter";
import TablePagination from "@material-ui/core/TablePagination";
import Alert from "@material-ui/lab/Alert";
import AlertTitle from "@material-ui/lab/AlertTitle";
import SyntaxHighlighter from "./SyntaxHighlighter";
import { AppState } from "../store";
import {
listCompletedTasksAsync,
deleteAllCompletedTasksAsync,
deleteCompletedTaskAsync,
batchDeleteCompletedTasksAsync,
} from "../actions/tasksActions";
import TablePaginationActions, {
rowsPerPageOptions,
} from "./TablePaginationActions";
import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions";
import {
durationFromSeconds,
stringifyDuration,
timeAgo,
uuidPrefix,
} from "../utils";
import { usePolling } from "../hooks";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
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 {
loading: state.tasks.completedTasks.loading,
error: state.tasks.completedTasks.error,
tasks: state.tasks.completedTasks.data,
batchActionPending: state.tasks.completedTasks.batchActionPending,
allActionPending: state.tasks.completedTasks.allActionPending,
pollInterval: state.settings.pollInterval,
pageSize: state.settings.taskRowsPerPage,
};
}
const mapDispatchToProps = {
listCompletedTasksAsync,
deleteCompletedTaskAsync,
deleteAllCompletedTasksAsync,
batchDeleteCompletedTasksAsync,
taskRowsPerPageChange,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
type ReduxProps = ConnectedProps<typeof connector>;
interface Props {
queue: string; // name of the queue.
totalTaskCount: number; // totoal number of completed tasks.
}
function CompletedTasksTable(props: Props & ReduxProps) {
const { pollInterval, listCompletedTasksAsync, queue, pageSize } = props;
const classes = useStyles();
const [page, setPage] = useState(0);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [activeTaskId, setActiveTaskId] = useState<string>("");
const handlePageChange = (
event: React.MouseEvent<HTMLButtonElement> | null,
newPage: number
) => {
setPage(newPage);
};
const handleRowsPerPageChange = (
event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
props.taskRowsPerPageChange(parseInt(event.target.value, 10));
setPage(0);
};
const handleSelectAllClick = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.checked) {
const newSelected = props.tasks.map((t) => t.id);
setSelectedIds(newSelected);
} else {
setSelectedIds([]);
}
};
const handleDeleteAllClick = () => {
props.deleteAllCompletedTasksAsync(queue);
};
const handleBatchDeleteClick = () => {
props
.batchDeleteCompletedTasksAsync(queue, selectedIds)
.then(() => setSelectedIds([]));
};
const fetchData = useCallback(() => {
const pageOpts = { page: page + 1, size: pageSize };
listCompletedTasksAsync(queue, pageOpts);
}, [page, pageSize, queue, listCompletedTasksAsync]);
usePolling(fetchData, pollInterval);
if (props.error.length > 0) {
return (
<Alert severity="error" className={classes.alert}>
<AlertTitle>Error</AlertTitle>
{props.error}
</Alert>
);
}
if (props.tasks.length === 0) {
return (
<Alert severity="info" className={classes.alert}>
<AlertTitle>Info</AlertTitle>
No completed tasks at this time.
</Alert>
);
}
const columns: TableColumn[] = [
{ key: "id", label: "ID", align: "left" },
{ key: "type", label: "Type", align: "left" },
{ key: "payload", label: "Payload", align: "left" },
{ key: "completed_at", label: "Completed", align: "left" },
{ key: "result", label: "Result", align: "left" },
{ key: "ttl", label: "TTL", align: "left" },
{ key: "actions", label: "Actions", align: "center" },
];
const rowCount = props.tasks.length;
const numSelected = selectedIds.length;
return (
<div>
<TableActions
showIconButtons={numSelected > 0}
iconButtonActions={[
{
tooltip: "Delete",
icon: <DeleteIcon />,
onClick: handleBatchDeleteClick,
disabled: props.batchActionPending,
},
]}
menuItemActions={[
{
label: "Delete All",
onClick: handleDeleteAllClick,
disabled: props.allActionPending,
},
]}
/>
<TableContainer component={Paper}>
<Table
stickyHeader={true}
className={classes.table}
aria-label="archived tasks table"
size="small"
>
<TableHead>
<TableRow>
<TableCell
padding="checkbox"
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
<IconButton>
<Checkbox
indeterminate={numSelected > 0 && numSelected < rowCount}
checked={rowCount > 0 && numSelected === rowCount}
onChange={handleSelectAllClick}
inputProps={{
"aria-label": "select all tasks shown in the table",
}}
/>
</IconButton>
</TableCell>
{columns.map((col) => (
<TableCell
key={col.key}
align={col.align}
classes={{ stickyHeader: classes.stickyHeaderCell }}
>
{col.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{props.tasks.map((task) => (
<Row
key={task.id}
task={task}
isSelected={selectedIds.includes(task.id)}
onSelectChange={(checked: boolean) => {
if (checked) {
setSelectedIds(selectedIds.concat(task.id));
} else {
setSelectedIds(selectedIds.filter((id) => id !== task.id));
}
}}
onDeleteClick={() => {
props.deleteCompletedTaskAsync(queue, task.id);
}}
allActionPending={props.allActionPending}
onActionCellEnter={() => setActiveTaskId(task.id)}
onActionCellLeave={() => setActiveTaskId("")}
showActions={activeTaskId === task.id}
/>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={rowsPerPageOptions}
colSpan={columns.length + 1}
count={props.totalTaskCount}
rowsPerPage={pageSize}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true,
}}
onPageChange={handlePageChange}
onRowsPerPageChange={handleRowsPerPageChange}
ActionsComponent={TablePaginationActions}
className={classes.pagination}
/>
</TableRow>
</TableFooter>
</Table>
</TableContainer>
</div>
);
}
const useRowStyles = makeStyles((theme) => ({
root: {
cursor: "pointer",
"&:hover": {
boxShadow: theme.shadows[2],
},
"&:hover .MuiTableCell-root": {
borderBottomColor: theme.palette.background.paper,
},
},
actionCell: {
width: "96px",
},
actionButton: {
marginLeft: 3,
marginRight: 3,
},
}));
interface RowProps {
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onDeleteClick: () => void;
allActionPending: boolean;
showActions: boolean;
onActionCellEnter: () => void;
onActionCellLeave: () => void;
}
function Row(props: RowProps) {
const { task } = props;
const classes = useRowStyles();
const history = useHistory();
return (
<TableRow
key={task.id}
className={classes.root}
selected={props.isSelected}
onClick={() => history.push(taskDetailsPath(task.queue, task.id))}
>
<TableCell padding="checkbox" onClick={(e) => e.stopPropagation()}>
<IconButton>
<Checkbox
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
props.onSelectChange(event.target.checked)
}
checked={props.isSelected}
/>
</IconButton>
</TableCell>
<TableCell component="th" scope="row">
{uuidPrefix(task.id)}
</TableCell>
<TableCell>{task.type}</TableCell>
<TableCell>
<SyntaxHighlighter
language="json"
customStyle={{ margin: 0, maxWidth: 400 }}
>
{task.payload}
</SyntaxHighlighter>
</TableCell>
<TableCell>{timeAgo(task.completed_at)}</TableCell>
<TableCell>
<SyntaxHighlighter
language="json"
customStyle={{ margin: 0, maxWidth: 400 }}
>
{task.result}
</SyntaxHighlighter>
</TableCell>
<TableCell>
{task.ttl_seconds > 0
? `${stringifyDuration(durationFromSeconds(task.ttl_seconds))} left`
: `expired`}
</TableCell>
<TableCell
align="center"
className={classes.actionCell}
onMouseEnter={props.onActionCellEnter}
onMouseLeave={props.onActionCellLeave}
onClick={(e) => e.stopPropagation()}
>
{props.showActions ? (
<React.Fragment>
<Tooltip title="Delete">
<IconButton
className={classes.actionButton}
onClick={props.onDeleteClick}
disabled={task.requestPending || props.allActionPending}
size="small"
>
<DeleteIcon fontSize="small" />
</IconButton>
</Tooltip>
</React.Fragment>
) : (
<IconButton size="small" onClick={props.onActionCellEnter}>
<MoreHorizIcon fontSize="small" />
</IconButton>
)}
</TableCell>
</TableRow>
);
}
export default connector(CompletedTasksTable);

View File

@@ -38,7 +38,7 @@ import { AppState } from "../store";
import { usePolling } from "../hooks";
import { uuidPrefix } from "../utils";
import { TableColumn } from "../types/table";
import { PendingTaskExtended } from "../reducers/tasksReducer";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { taskDetailsPath } from "../paths";
const useStyles = makeStyles((theme) => ({
@@ -313,7 +313,7 @@ const useRowStyles = makeStyles((theme) => ({
}));
interface RowProps {
task: PendingTaskExtended;
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onDeleteClick: () => void;

View File

@@ -23,6 +23,7 @@ interface TaskBreakdown {
scheduled: number; // number of scheduled tasks in the queue.
retry: number; // number of retry tasks in the queue.
archived: number; // number of archived tasks in the queue.
completed: number; // number of completed tasks in the queue.
}
function QueueSizeChart(props: Props) {
@@ -55,6 +56,7 @@ function QueueSizeChart(props: Props) {
<Bar dataKey="scheduled" stackId="a" fill="#fdd663" />
<Bar dataKey="retry" stackId="a" fill="#f666a9" />
<Bar dataKey="archived" stackId="a" fill="#ac4776" />
<Bar dataKey="completed" stackId="a" fill="#4bb543" />
</BarChart>
</ResponsiveContainer>
);

View File

@@ -41,7 +41,7 @@ import { taskRowsPerPageChange } from "../actions/settingsActions";
import TableActions from "./TableActions";
import { durationBefore, uuidPrefix } from "../utils";
import { usePolling } from "../hooks";
import { RetryTaskExtended } from "../reducers/tasksReducer";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
@@ -344,7 +344,7 @@ const useRowStyles = makeStyles((theme) => ({
}));
interface RowProps {
task: RetryTaskExtended;
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onDeleteClick: () => void;

View File

@@ -41,7 +41,7 @@ import TablePaginationActions, {
import TableActions from "./TableActions";
import { durationBefore, uuidPrefix } from "../utils";
import { usePolling } from "../hooks";
import { ScheduledTaskExtended } from "../reducers/tasksReducer";
import { TaskInfoExtended } from "../reducers/tasksReducer";
import { TableColumn } from "../types/table";
import { taskDetailsPath } from "../paths";
@@ -341,7 +341,7 @@ const useRowStyles = makeStyles((theme) => ({
}));
interface RowProps {
task: ScheduledTaskExtended;
task: TaskInfoExtended;
isSelected: boolean;
onSelectChange: (checked: boolean) => void;
onRunClick: () => void;

View File

@@ -11,6 +11,7 @@ import PendingTasksTable from "./PendingTasksTable";
import ScheduledTasksTable from "./ScheduledTasksTable";
import RetryTasksTable from "./RetryTasksTable";
import ArchivedTasksTable from "./ArchivedTasksTable";
import CompletedTasksTable from "./CompletedTasksTable";
import { useHistory } from "react-router-dom";
import { queueDetailsPath, taskDetailsPath } from "../paths";
import { QueueInfo } from "../reducers/queuesReducer";
@@ -56,6 +57,7 @@ function mapStatetoProps(state: AppState, ownProps: Props) {
scheduled: 0,
retry: 0,
archived: 0,
completed: 0,
processed: 0,
failed: 0,
timestamp: "n/a",
@@ -104,7 +106,8 @@ const useStyles = makeStyles((theme) => ({
marginLeft: "2px",
},
searchbar: {
marginLeft: theme.spacing(4),
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(2),
},
search: {
position: "relative",
@@ -147,6 +150,7 @@ function TasksTable(props: Props & ReduxProps) {
{ key: "scheduled", label: "Scheduled", count: currentStats.scheduled },
{ key: "retry", label: "Retry", count: currentStats.retry },
{ key: "archived", label: "Archived", count: currentStats.archived },
{ key: "completed", label: "Completed", count: currentStats.completed },
];
const [searchQuery, setSearchQuery] = useState<string>("");
@@ -229,6 +233,12 @@ function TasksTable(props: Props & ReduxProps) {
totalTaskCount={currentStats.archived}
/>
</TabPanel>
<TabPanel value="completed" selected={props.selected}>
<CompletedTasksTable
queue={props.queue}
totalTaskCount={currentStats.completed}
/>
</TabPanel>
</Paper>
);
}

View File

@@ -74,5 +74,11 @@ export default function queueStatsReducer(
// Returns true if two timestamps are from the same date.
function isSameDate(ts1: string, ts2: string): boolean {
return new Date(ts1).toDateString() === new Date(ts2).toDateString();
const date1 = new Date(ts1);
const date2 = new Date(ts2);
return (
date1.getUTCDate() === date2.getUTCDate() &&
date1.getUTCMonth() === date2.getUTCMonth() &&
date1.getUTCFullYear() === date2.getUTCFullYear()
);
}

View File

@@ -50,6 +50,9 @@ import {
BATCH_DELETE_PENDING_TASKS_SUCCESS,
ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
DELETE_ALL_PENDING_TASKS_SUCCESS,
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
} from "../actions/tasksActions";
import { Queue } from "../api";
@@ -550,8 +553,7 @@ function queuesReducer(
queueInfo.currentStats.pending +
action.payload.archived_ids.length,
retry:
queueInfo.currentStats.retry -
action.payload.archived_ids.length,
queueInfo.currentStats.retry - action.payload.archived_ids.length,
},
};
});
@@ -647,6 +649,23 @@ function queuesReducer(
return { ...state, data: newData };
}
case DELETE_COMPLETED_TASK_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
size: queueInfo.currentStats.size - 1,
completed: queueInfo.currentStats.completed - 1,
},
};
});
return { ...state, data: newData };
}
case BATCH_RUN_ARCHIVED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@@ -688,6 +707,26 @@ function queuesReducer(
return { ...state, data: newData };
}
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
size:
queueInfo.currentStats.size - action.payload.deleted_ids.length,
completed:
queueInfo.currentStats.completed -
action.payload.deleted_ids.length,
},
};
});
return { ...state, data: newData };
}
case RUN_ALL_ARCHIVED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
@@ -723,6 +762,23 @@ function queuesReducer(
return { ...state, data: newData };
}
case DELETE_ALL_COMPLETED_TASKS_SUCCESS: {
const newData = state.data.map((queueInfo) => {
if (queueInfo.name !== action.queue) {
return queueInfo;
}
return {
...queueInfo,
currentStats: {
...queueInfo.currentStats,
size: queueInfo.currentStats.size - action.deleted,
completed: 0,
},
};
});
return { ...state, data: newData };
}
default:
return state;
}

View File

@@ -36,6 +36,9 @@ import {
BATCH_DELETE_PENDING_TASKS_SUCCESS,
ARCHIVE_ALL_PENDING_TASKS_SUCCESS,
DELETE_ALL_PENDING_TASKS_SUCCESS,
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
} from "../actions/tasksActions";
interface SnackbarState {
@@ -285,6 +288,25 @@ function snackbarReducer(
message: "All archived tasks deleted",
};
case DELETE_COMPLETED_TASK_SUCCESS:
return {
isOpen: true,
message: `Completed task deleted`,
};
case DELETE_ALL_COMPLETED_TASKS_SUCCESS:
return {
isOpen: true,
message: "All completed tasks deleted",
};
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS:
const n = action.payload.deleted_ids.length;
return {
isOpen: true,
message: `${n} completed ${n === 1 ? "task" : "tasks"} deleted`,
};
default:
return state;
}

View File

@@ -15,6 +15,9 @@ import {
LIST_ARCHIVED_TASKS_BEGIN,
LIST_ARCHIVED_TASKS_SUCCESS,
LIST_ARCHIVED_TASKS_ERROR,
LIST_COMPLETED_TASKS_BEGIN,
LIST_COMPLETED_TASKS_SUCCESS,
LIST_COMPLETED_TASKS_ERROR,
CANCEL_ACTIVE_TASK_BEGIN,
CANCEL_ACTIVE_TASK_SUCCESS,
CANCEL_ACTIVE_TASK_ERROR,
@@ -117,17 +120,19 @@ import {
GET_TASK_INFO_BEGIN,
GET_TASK_INFO_ERROR,
GET_TASK_INFO_SUCCESS,
DELETE_COMPLETED_TASK_BEGIN,
DELETE_COMPLETED_TASK_ERROR,
DELETE_COMPLETED_TASK_SUCCESS,
DELETE_ALL_COMPLETED_TASKS_BEGIN,
DELETE_ALL_COMPLETED_TASKS_ERROR,
DELETE_ALL_COMPLETED_TASKS_SUCCESS,
BATCH_DELETE_COMPLETED_TASKS_BEGIN,
BATCH_DELETE_COMPLETED_TASKS_ERROR,
BATCH_DELETE_COMPLETED_TASKS_SUCCESS,
} from "../actions/tasksActions";
import {
ActiveTask,
ArchivedTask,
PendingTask,
RetryTask,
ScheduledTask,
TaskInfo,
} from "../api";
import { TaskInfo } from "../api";
export interface ActiveTaskExtended extends ActiveTask {
export interface ActiveTaskExtended extends TaskInfo {
// Indicates that a request has been sent for this
// task and awaiting for a response.
requestPending: boolean;
@@ -137,25 +142,7 @@ 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.
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 ArchivedTaskExtended extends ArchivedTask {
export interface TaskInfoExtended extends TaskInfo {
// Indicates that a request has been sent for this
// task and awaiting for a response.
requestPending: boolean;
@@ -174,34 +161,41 @@ interface TasksState {
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: PendingTaskExtended[];
data: TaskInfoExtended[];
};
scheduledTasks: {
loading: boolean;
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: ScheduledTaskExtended[];
data: TaskInfoExtended[];
};
retryTasks: {
loading: boolean;
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: RetryTaskExtended[];
data: TaskInfoExtended[];
};
archivedTasks: {
loading: boolean;
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: ArchivedTaskExtended[];
data: TaskInfoExtended[];
};
completedTasks: {
loading: boolean;
batchActionPending: boolean;
allActionPending: boolean;
error: string;
data: TaskInfoExtended[];
};
taskInfo: {
loading: boolean;
error: string;
data?: TaskInfo;
},
};
}
const initialState: TasksState = {
@@ -240,10 +234,17 @@ const initialState: TasksState = {
error: "",
data: [],
},
completedTasks: {
loading: false,
batchActionPending: false,
allActionPending: false,
error: "",
data: [],
},
taskInfo: {
loading: false,
error: "",
}
},
};
function tasksReducer(
@@ -258,16 +259,16 @@ function tasksReducer(
...state.taskInfo,
loading: true,
},
}
};
case GET_TASK_INFO_ERROR:
return {
...state,
taskInfo: {
loading: false,
error: action.error,
},
};
return {
...state,
taskInfo: {
loading: false,
error: action.error,
},
};
case GET_TASK_INFO_SUCCESS:
return {
@@ -450,6 +451,157 @@ function tasksReducer(
},
};
case LIST_COMPLETED_TASKS_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
loading: true,
},
};
case LIST_COMPLETED_TASKS_SUCCESS:
return {
...state,
completedTasks: {
...state.completedTasks,
loading: false,
error: "",
data: action.payload.tasks.map((task) => ({
...task,
requestPending: false,
})),
},
};
case LIST_COMPLETED_TASKS_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
loading: false,
error: action.error,
data: [],
},
};
case DELETE_COMPLETED_TASK_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
data: state.completedTasks.data.map((task) => {
if (task.id !== action.taskId) {
return task;
}
return { ...task, requestPending: true };
}),
},
};
case DELETE_COMPLETED_TASK_SUCCESS:
return {
...state,
completedTasks: {
...state.completedTasks,
data: state.completedTasks.data.filter(
(task) => task.id !== action.taskId
),
},
};
case DELETE_COMPLETED_TASK_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
data: state.completedTasks.data.map((task) => {
if (task.id !== action.taskId) {
return task;
}
return { ...task, requestPending: false };
}),
},
};
case DELETE_ALL_COMPLETED_TASKS_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
allActionPending: true,
},
};
case DELETE_ALL_COMPLETED_TASKS_SUCCESS:
return {
...state,
completedTasks: {
...state.completedTasks,
allActionPending: false,
data: [],
},
};
case DELETE_ALL_COMPLETED_TASKS_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
allActionPending: false,
},
};
case BATCH_DELETE_COMPLETED_TASKS_BEGIN:
return {
...state,
completedTasks: {
...state.completedTasks,
batchActionPending: true,
data: state.completedTasks.data.map((task) => {
if (!action.taskIds.includes(task.id)) {
return task;
}
return {
...task,
requestPending: true,
};
}),
},
};
case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: {
const newData = state.completedTasks.data.filter(
(task) => !action.payload.deleted_ids.includes(task.id)
);
return {
...state,
completedTasks: {
...state.completedTasks,
batchActionPending: false,
data: newData,
},
};
}
case BATCH_DELETE_COMPLETED_TASKS_ERROR:
return {
...state,
completedTasks: {
...state.completedTasks,
batchActionPending: false,
data: state.completedTasks.data.map((task) => {
if (!action.taskIds.includes(task.id)) {
return task;
}
return {
...task,
requestPending: false,
};
}),
},
};
case CANCEL_ACTIVE_TASK_BEGIN: {
const newData = state.activeTasks.data.map((task) => {
if (task.id !== action.taskId) {

View File

@@ -25,17 +25,22 @@ interface Duration {
totalSeconds: number;
}
// start and end are in milliseconds.
function durationBetween(start: number, end: number): Duration {
const durationInMillisec = start - end;
const totalSeconds = Math.floor(durationInMillisec / 1000);
// Returns a duration from the number of seconds provided.
export function durationFromSeconds(totalSeconds: number): Duration {
const hour = Math.floor(totalSeconds / 3600);
const minute = Math.floor((totalSeconds - 3600 * hour) / 60);
const second = totalSeconds - 3600 * hour - 60 * minute;
return { hour, minute, second, totalSeconds };
}
function stringifyDuration(d: Duration): string {
// start and end are in milliseconds.
function durationBetween(start: number, end: number): Duration {
const durationInMillisec = start - end;
const totalSeconds = Math.floor(durationInMillisec / 1000);
return durationFromSeconds(totalSeconds);
}
export function stringifyDuration(d: Duration): string {
if (d.hour > 24) {
const n = Math.floor(d.hour / 24);
return n + (n === 1 ? " day" : " days");

View File

@@ -18,6 +18,7 @@ import { TaskDetailsRouteParams } from "../paths";
import { usePolling } from "../hooks";
import { listQueuesAsync } from "../actions/queuesActions";
import SyntaxHighlighter from "../components/SyntaxHighlighter";
import { durationFromSeconds, stringifyDuration, timeAgo } from "../utils";
function mapStateToProps(state: AppState) {
return {
@@ -175,7 +176,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.error_message} ({taskInfo?.last_failed_at})
</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</Typography>
</div>
@@ -189,7 +190,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.next_process_at ? (
<Typography>{taskInfo?.next_process_at}</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</div>
</div>
@@ -201,7 +202,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.timeout_seconds ? (
<Typography>{taskInfo?.timeout_seconds} seconds</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</Typography>
</div>
@@ -213,7 +214,7 @@ function TaskDetailsView(props: Props) {
{taskInfo?.deadline ? (
<Typography>{taskInfo?.deadline}</Typography>
) : (
<Typography>n/a</Typography>
<Typography> - </Typography>
)}
</Typography>
</div>
@@ -232,6 +233,59 @@ function TaskDetailsView(props: Props) {
)}
</div>
</div>
{
/* Completed Task Only */ taskInfo?.state === "completed" && (
<>
<div className={classes.infoRow}>
<Typography
variant="subtitle2"
className={classes.infoKeyCell}
>
Completed:{" "}
</Typography>
<div className={classes.infoValueCell}>
<Typography>
{timeAgo(taskInfo.completed_at)} (
{taskInfo.completed_at})
</Typography>
</div>
</div>
<div className={classes.infoRow}>
<Typography
variant="subtitle2"
className={classes.infoKeyCell}
>
Result:{" "}
</Typography>
<div className={classes.infoValueCell}>
<SyntaxHighlighter
language="json"
customStyle={{ margin: 0, maxWidth: 400 }}
>
{taskInfo.result}
</SyntaxHighlighter>
</div>
</div>
<div className={classes.infoRow}>
<Typography
variant="subtitle2"
className={classes.infoKeyCell}
>
TTL:{" "}
</Typography>
<Typography className={classes.infoValueCell}>
<Typography>
{taskInfo.ttl_seconds > 0
? `${stringifyDuration(
durationFromSeconds(taskInfo.ttl_seconds)
)} left`
: "expired"}
</Typography>
</Typography>
</div>
</>
)
}
</Paper>
)}
<div className={classes.footer}>

View File

@@ -38,7 +38,14 @@ function useQuery(): URLSearchParams {
return new URLSearchParams(useLocation().search);
}
const validStatus = ["active", "pending", "scheduled", "retry", "archived"];
const validStatus = [
"active",
"pending",
"scheduled",
"retry",
"archived",
"completed",
];
const defaultStatus = "active";
function TasksView(props: ConnectedProps<typeof connector>) {