From 2ec7a68d4a3223fc738ac36266efb579852179a8 Mon Sep 17 00:00:00 2001 From: Andy Bao Date: Tue, 17 May 2022 19:19:28 -0700 Subject: [PATCH] Implement in-browser task search --- ui/src/actions/tasksActions.ts | 268 +++++++++++++++- ui/src/components/TasksFilterDialog.tsx | 165 ++++++++++ .../components/TasksFilterProgressDialog.tsx | 55 ++++ ui/src/components/TasksTable.tsx | 85 +++-- ui/src/components/TasksTableContainer.tsx | 145 ++++++++- ui/src/reducers/filterReducer.ts | 149 +++++++++ ui/src/reducers/queuesReducer.ts | 82 +++-- ui/src/reducers/tasksReducer.ts | 298 +++++++++--------- ui/src/store.ts | 6 + 9 files changed, 1016 insertions(+), 237 deletions(-) create mode 100644 ui/src/components/TasksFilterDialog.tsx create mode 100644 ui/src/components/TasksFilterProgressDialog.tsx create mode 100644 ui/src/reducers/filterReducer.ts diff --git a/ui/src/actions/tasksActions.ts b/ui/src/actions/tasksActions.ts index 51d771b..17ef01e 100644 --- a/ui/src/actions/tasksActions.ts +++ b/ui/src/actions/tasksActions.ts @@ -60,9 +60,11 @@ import { runAggregatingTask, archiveAggregatingTask, ListAggregatingTasksResponse, + Queue, } from "../api"; import { Dispatch } from "redux"; import { toErrorString, toErrorStringWithHttpStatus } from "../utils"; +import { AppState } from "../store"; // List of tasks related action types. export const GET_TASK_INFO_BEGIN = "GET_TASK_INFO_BEGIN"; @@ -89,6 +91,11 @@ export const LIST_COMPLETED_TASKS_ERROR = "LIST_COMPLETED_TASKS_ERROR"; export const LIST_AGGREGATING_TASKS_BEGIN = "LIST_AGGREGATING_TASKS_BEGIN"; export const LIST_AGGREGATING_TASKS_SUCCESS = "LIST_AGGREGATING_TASKS_SUCCESS"; export const LIST_AGGREGATING_TASKS_ERROR = "LIST_AGGREGATING_TASKS_ERROR"; +export const FILTER_TASKS_BEGIN = "FILTER_TASKS_BEGIN"; +export const FILTER_TASKS_PROGRESS = "FILTER_TASKS_PROGRESS"; +export const FILTER_TASKS_SUCCESS = "FILTER_TASKS_SUCCESS"; +export const FILTER_TASKS_ERROR = "FILTER_TASKS_ERROR"; +export const FILTER_TASKS_CANCEL = "FILTER_TASKS_CANCEL"; 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"; @@ -429,6 +436,30 @@ interface ListAggregatingTasksErrorAction { error: string; // error description } +interface FilterTasksBeginAction { + type: typeof FILTER_TASKS_BEGIN; +} + +interface FilterTasksProgressAction { + type: typeof FILTER_TASKS_PROGRESS; + processedTasks: number; + filterResults: TaskInfo[]; + newStats: Queue; +} + +interface FilterTasksSuccessAction { + type: typeof FILTER_TASKS_SUCCESS; +} + +interface FilterTasksErrorAction { + type: typeof FILTER_TASKS_ERROR; + error: string; +} + +interface FilterTasksCancelAction { + type: typeof FILTER_TASKS_CANCEL; +} + interface CancelActiveTaskBeginAction { type: typeof CANCEL_ACTIVE_TASK_BEGIN; queue: string; @@ -1291,6 +1322,11 @@ export type TasksActionTypes = | ListAggregatingTasksBeginAction | ListAggregatingTasksSuccessAction | ListAggregatingTasksErrorAction + | FilterTasksBeginAction + | FilterTasksProgressAction + | FilterTasksSuccessAction + | FilterTasksErrorAction + | FilterTasksCancelAction | CancelActiveTaskBeginAction | CancelActiveTaskSuccessAction | CancelActiveTaskErrorAction @@ -1446,14 +1482,42 @@ export function getTaskInfoAsync(qname: string, id: string) { }; } +function getFilterResults( + state: AppState, + qname: string, + pageOpts?: PaginationOptions +): ListTasksResponse | null { + const filterOp = state.tasks.filterOp; + if (filterOp == null || !filterOp.done) return null; + const curQueueStats = state.queues.data.find((it) => it.name === qname); + if (curQueueStats == null) return null; + + const size = pageOpts?.size ?? 20; + const page = pageOpts?.page ?? 1; + + const start = (page - 1) * size; + const end = start + size; + const results = filterOp.result.slice(start, end); + + return { + tasks: results, + stats: curQueueStats.currentStats, + }; +} + export function listActiveTasksAsync( qname: string, pageOpts?: PaginationOptions ) { - return async (dispatch: Dispatch) => { + return async ( + dispatch: Dispatch, + getState: () => AppState + ) => { dispatch({ type: LIST_ACTIVE_TASKS_BEGIN, queue: qname }); try { - const response = await listActiveTasks(qname, pageOpts); + const response = + getFilterResults(getState(), qname, pageOpts) ?? + (await listActiveTasks(qname, pageOpts)); dispatch({ type: LIST_ACTIVE_TASKS_SUCCESS, queue: qname, @@ -1477,10 +1541,15 @@ export function listPendingTasksAsync( qname: string, pageOpts?: PaginationOptions ) { - return async (dispatch: Dispatch) => { + return async ( + dispatch: Dispatch, + getState: () => AppState + ) => { dispatch({ type: LIST_PENDING_TASKS_BEGIN, queue: qname }); try { - const response = await listPendingTasks(qname, pageOpts); + const response = + getFilterResults(getState(), qname, pageOpts) ?? + (await listPendingTasks(qname, pageOpts)); dispatch({ type: LIST_PENDING_TASKS_SUCCESS, queue: qname, @@ -1504,10 +1573,15 @@ export function listScheduledTasksAsync( qname: string, pageOpts?: PaginationOptions ) { - return async (dispatch: Dispatch) => { + return async ( + dispatch: Dispatch, + getState: () => AppState + ) => { dispatch({ type: LIST_SCHEDULED_TASKS_BEGIN, queue: qname }); try { - const response = await listScheduledTasks(qname, pageOpts); + const response = + getFilterResults(getState(), qname, pageOpts) ?? + (await listScheduledTasks(qname, pageOpts)); dispatch({ type: LIST_SCHEDULED_TASKS_SUCCESS, queue: qname, @@ -1531,10 +1605,15 @@ export function listRetryTasksAsync( qname: string, pageOpts?: PaginationOptions ) { - return async (dispatch: Dispatch) => { + return async ( + dispatch: Dispatch, + getState: () => AppState + ) => { dispatch({ type: LIST_RETRY_TASKS_BEGIN, queue: qname }); try { - const response = await listRetryTasks(qname, pageOpts); + const response = + getFilterResults(getState(), qname, pageOpts) ?? + (await listRetryTasks(qname, pageOpts)); dispatch({ type: LIST_RETRY_TASKS_SUCCESS, queue: qname, @@ -1558,10 +1637,15 @@ export function listArchivedTasksAsync( qname: string, pageOpts?: PaginationOptions ) { - return async (dispatch: Dispatch) => { + return async ( + dispatch: Dispatch, + getState: () => AppState + ) => { dispatch({ type: LIST_ARCHIVED_TASKS_BEGIN, queue: qname }); try { - const response = await listArchivedTasks(qname, pageOpts); + const response = + getFilterResults(getState(), qname, pageOpts) ?? + (await listArchivedTasks(qname, pageOpts)); dispatch({ type: LIST_ARCHIVED_TASKS_SUCCESS, queue: qname, @@ -1585,10 +1669,15 @@ export function listCompletedTasksAsync( qname: string, pageOpts?: PaginationOptions ) { - return async (dispatch: Dispatch) => { + return async ( + dispatch: Dispatch, + getState: () => AppState + ) => { try { dispatch({ type: LIST_COMPLETED_TASKS_BEGIN, queue: qname }); - const response = await listCompletedTasks(qname, pageOpts); + const response = + getFilterResults(getState(), qname, pageOpts) ?? + (await listCompletedTasks(qname, pageOpts)); dispatch({ type: LIST_COMPLETED_TASKS_SUCCESS, queue: qname, @@ -1613,6 +1702,7 @@ export function listAggregatingTasksAsync( gname: string, pageOpts?: PaginationOptions ) { + // TODO Add filter support return async (dispatch: Dispatch) => { try { dispatch({ @@ -1642,6 +1732,160 @@ export function listAggregatingTasksAsync( }; } +export interface TasksFilter { + payloadQuery?: string; + resultQuery?: string; + payloadRegex?: RegExp; + resultRegex?: RegExp; + customJs?: string; + resultLimit?: number; +} + +function parseIfJson(str: string) { + try { + return JSON.parse(str); + } catch (e) { + return str; + } +} + +function evalCustomJsFilter(js: string, task: TaskInfo): any { + /* eslint-disable @typescript-eslint/no-unused-vars */ + const { + id, + queue, + type, + state, + start_time, + max_retry, + retried, + last_failed_at, + error_message, + next_process_at, + timeout_seconds, + deadline, + group, + completed_at, + ttl_seconds, + is_orphaned, + } = task; + // Parse payload and result into JSON for convenience + const payload = parseIfJson(task.payload); + const result = parseIfJson(task.result); + /* eslint-enable @typescript-eslint/no-unused-vars */ + return eval(js); // eslint-disable-line no-eval +} + +function filterTask(filter: TasksFilter, task: TaskInfo): boolean { + if ( + filter.payloadQuery != null && + !task.payload.includes(filter.payloadQuery) + ) { + return false; + } + if (filter.resultQuery != null && !task.result.includes(filter.resultQuery)) { + return false; + } + if ( + filter.payloadRegex != null && + task.payload.match(filter.payloadRegex) == null + ) { + return false; + } + if ( + filter.resultRegex != null && + task.result.match(filter.resultRegex) == null + ) { + return false; + } + if (filter.customJs != null) { + let customJsResult; + try { + customJsResult = evalCustomJsFilter(filter.customJs, task); + } catch (e) { + console.error( + "Custom JS filter error:", + e, + "task:", + task, + "skipping task." + ); + return false; + } + // Custom JS can return anything so sanitize output into a boolean + return !!customJsResult; + } else { + return true; + } +} + +export function filterTasksAsync( + filter: TasksFilter, + fetchTasks: ( + page?: number // page number (1 being the first page) + ) => Promise +) { + return async ( + dispatch: Dispatch, + getState: () => AppState + ) => { + dispatch({ type: FILTER_TASKS_BEGIN }); + + let page = 1; + let processed = 0; + let finished = false; + do { + // Check if operation was cancelled, if so, return + if (getState().tasks.filterOp == null) return; + + // Fetch next page + let response; + try { + response = await fetchTasks(page); + } catch (error) { + dispatch({ + type: FILTER_TASKS_ERROR, + error: toErrorString(error), + }); + return; + } + page++; + + // Process page + const filterResults: TaskInfo[] = []; + for (const task of response.tasks) { + if (filterTask(filter, task)) { + filterResults.push(task); + processed++; + if (filter.resultLimit != null && processed >= filter.resultLimit) { + finished = true; + break; + } + } + } + + // Update state + dispatch({ + type: FILTER_TASKS_PROGRESS, + processedTasks: response.tasks.length, + newStats: response.stats, + filterResults, + }); + if (response.tasks.length === 0) finished = true; + } while (!finished); + + dispatch({ + type: FILTER_TASKS_SUCCESS, + }); + }; +} + +export function cancelFilterTasks() { + return { + type: FILTER_TASKS_CANCEL, + }; +} + export function cancelActiveTaskAsync(queue: string, taskId: string) { return async (dispatch: Dispatch) => { dispatch({ type: CANCEL_ACTIVE_TASK_BEGIN, queue, taskId }); diff --git a/ui/src/components/TasksFilterDialog.tsx b/ui/src/components/TasksFilterDialog.tsx new file mode 100644 index 0000000..0fcdac8 --- /dev/null +++ b/ui/src/components/TasksFilterDialog.tsx @@ -0,0 +1,165 @@ +import React, { + ChangeEventHandler, + KeyboardEventHandler, + useState, +} from "react"; +import Dialog from "@material-ui/core/Dialog"; +import DialogTitle from "@material-ui/core/DialogTitle"; +import TextField from "@material-ui/core/TextField"; +import DialogContent from "@material-ui/core/DialogContent"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import { TasksFilter } from "../actions/tasksActions"; +import DialogContentText from "@material-ui/core/DialogContentText"; + +interface TasksFilterDialogProps { + open: boolean; + onClose?: () => void; + onFilter?: (filter: TasksFilter) => void; +} + +function TasksFilterDialog(props: TasksFilterDialogProps) { + const [customJs, setCustomJs] = useState(""); + const [payloadQuery, setPayloadQuery] = useState(""); + const [resultQuery, setResultQuery] = useState(""); + const [payloadRegex, setPayloadRegex] = useState(""); + const [resultRegex, setResultRegex] = useState(""); + const [resultLimitStr, setResultLimitStr] = useState(""); + const [resultLimit, setResultLimit] = useState(-1); + const [resultLimitError, setResultLimitError] = useState(null); + + const buildFilter = () => { + if (resultLimitError != null) return null; + + const filter: TasksFilter = {}; + if (payloadQuery.trim().length > 0) filter.payloadQuery = payloadQuery; + if (resultQuery.trim().length > 0) filter.resultQuery = resultQuery; + if (payloadRegex.trim().length > 0) + filter.payloadRegex = RegExp(payloadRegex); + if (resultRegex.trim().length > 0) filter.resultRegex = RegExp(resultRegex); + if (customJs.trim().length > 0) filter.customJs = customJs; + if (resultLimit >= 0) filter.resultLimit = resultLimit; + return filter; + }; + + const handleRunFilter = () => { + const filter = buildFilter(); + if (filter != null) { + if (props.onClose != null) props.onClose(); + if (props.onFilter != null) props.onFilter(filter); + } + }; + + const handleKeyDown: KeyboardEventHandler = (event) => { + if (event.key === "Enter") { + handleRunFilter(); + } + }; + + const handleResultLimitChange: ChangeEventHandler = ( + event + ) => { + const value = event.target.value; + setResultLimitStr(value); + const trimmedValue = value.trim(); + if (trimmedValue.length === 0) { + setResultLimit(-1); + setResultLimitError(null); + } else { + const parsed = parseInt(trimmedValue, 10); + if (isNaN(parsed) || parsed < 0) { + setResultLimit(-1); + setResultLimitError("Please enter a valid positive number."); + } else { + setResultLimit(parsed); + setResultLimitError(null); + } + } + }; + + return ( + + Filter tasks + + +

+ Filter results are stored in browser memory, please ensure that your + filter does not return too many results. +

+

+ This feature may not behave correctly if the payload or result are + truncated due to MaxPayloadLength or{" "} + MaxResultLength being misconfigured. +

+
+ setPayloadQuery(e.target.value)} + onKeyDown={handleKeyDown} + /> + setResultQuery(e.target.value)} + onKeyDown={handleKeyDown} + /> + setPayloadRegex(e.target.value)} + onKeyDown={handleKeyDown} + /> + setResultRegex(e.target.value)} + onKeyDown={handleKeyDown} + /> + setCustomJs(e.target.value)} + /> + +
+ + + + +
+ ); +} + +export default TasksFilterDialog; diff --git a/ui/src/components/TasksFilterProgressDialog.tsx b/ui/src/components/TasksFilterProgressDialog.tsx new file mode 100644 index 0000000..57b2560 --- /dev/null +++ b/ui/src/components/TasksFilterProgressDialog.tsx @@ -0,0 +1,55 @@ +import DialogTitle from "@material-ui/core/DialogTitle"; +import React from "react"; +import DialogActions from "@material-ui/core/DialogActions"; +import Button from "@material-ui/core/Button"; +import Dialog from "@material-ui/core/Dialog"; +import DialogContent from "@material-ui/core/DialogContent"; +import LinearProgress from "@material-ui/core/LinearProgress"; +import { AppState } from "../store"; +import { connect, ConnectedProps } from "react-redux"; +import DialogContentText from "@material-ui/core/DialogContentText"; +import { cancelFilterTasks } from "../actions/tasksActions"; + +function mapStateToProps(state: AppState) { + const filterOp = state.tasks.filterOp; + return { + open: filterOp != null && !filterOp.done, + processedTasks: filterOp?.processedTasks ?? 0, + matches: filterOp?.result?.length ?? 0, + }; +} + +const mapDispatchToProps = { + cancelFilterTasks, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); +type ReduxProps = ConnectedProps; + +interface TasksFilterDialogProps extends ReduxProps { + totalTasks: number; +} + +function TasksFilterDialog(props: TasksFilterDialogProps) { + let progress; + if (props.totalTasks > 0) progress = props.processedTasks / props.totalTasks; + else progress = 0; + + return ( + + Running filter + + + Processed {props.processedTasks} of {props.totalTasks} total tasks,{" "} + {props.matches} matches so far. + + + + + + + + ); +} + +export default connector(TasksFilterDialog); diff --git a/ui/src/components/TasksTable.tsx b/ui/src/components/TasksTable.tsx index 27e1baf..42f2cb8 100644 --- a/ui/src/components/TasksTable.tsx +++ b/ui/src/components/TasksTable.tsx @@ -26,6 +26,18 @@ import { TaskInfoExtended } from "../reducers/tasksReducer"; import { TableColumn } from "../types/table"; import { PaginationOptions } from "../api"; import { TaskState } from "../types/taskState"; +import TasksFilterProgressDialog from "./TasksFilterProgressDialog"; +import { AppState } from "../store"; +import { connect, ConnectedProps } from "react-redux"; + +function mapStateToProps(state: AppState) { + return { + filterEnabled: state.tasks.filterOp?.done ?? false, + }; +} + +const connector = connect(mapStateToProps); +type ReduxProps = ConnectedProps; const useStyles = makeStyles((theme) => ({ table: { @@ -43,7 +55,7 @@ const useStyles = makeStyles((theme) => ({ }, })); -interface Props { +interface Props extends ReduxProps { queue: string; // name of the queue. totalTaskCount: number; // totoal number of tasks in the given state. taskState: TaskState; @@ -75,12 +87,15 @@ interface Props { renderRow: (rowProps: RowProps) => JSX.Element; } -export default function TasksTable(props: Props) { +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 [lastFilterEnabled, setLastFilterEnabled] = useState( + props.filterEnabled + ); const handlePageChange = ( event: React.MouseEvent | null, @@ -123,33 +138,36 @@ export default function TasksTable(props: Props) { } let allActions = []; - if (props.deleteAllTasks) { - allActions.push({ - label: "Delete All", - onClick: createAllActionHandler(props.deleteAllTasks), - disabled: props.allActionPending, - }); - } - if (props.archiveAllTasks) { - allActions.push({ - label: "Archive All", - onClick: createAllActionHandler(props.archiveAllTasks), - disabled: props.allActionPending, - }); - } - if (props.runAllTasks) { - allActions.push({ - label: "Run All", - onClick: createAllActionHandler(props.runAllTasks), - disabled: props.allActionPending, - }); - } - if (props.cancelAllTasks) { - allActions.push({ - label: "Cancel All", - onClick: createAllActionHandler(props.cancelAllTasks), - disabled: props.allActionPending, - }); + if (!props.filterEnabled) { + // Protect against using all actions while filter enabled + if (props.deleteAllTasks) { + allActions.push({ + label: "Delete All", + onClick: createAllActionHandler(props.deleteAllTasks), + disabled: props.allActionPending, + }); + } + if (props.archiveAllTasks) { + allActions.push({ + label: "Archive All", + onClick: createAllActionHandler(props.archiveAllTasks), + disabled: props.allActionPending, + }); + } + if (props.runAllTasks) { + allActions.push({ + label: "Run All", + onClick: createAllActionHandler(props.runAllTasks), + disabled: props.allActionPending, + }); + } + if (props.cancelAllTasks) { + allActions.push({ + label: "Cancel All", + onClick: createAllActionHandler(props.cancelAllTasks), + disabled: props.allActionPending, + }); + } } let batchActions = []; @@ -193,6 +211,12 @@ export default function TasksTable(props: Props) { usePolling(fetchData, pollInterval); + if (props.filterEnabled !== lastFilterEnabled) { + setLastFilterEnabled(props.filterEnabled); + setPage(0); + fetchData(); + } + if (props.error.length > 0) { return ( @@ -320,6 +344,7 @@ export default function TasksTable(props: Props) { + ); } @@ -376,3 +401,5 @@ export interface RowProps { onActionCellEnter: () => void; onActionCellLeave: () => void; } + +export default connector(TasksTable); diff --git a/ui/src/components/TasksTableContainer.tsx b/ui/src/components/TasksTableContainer.tsx index 24d9e27..1edda12 100644 --- a/ui/src/components/TasksTableContainer.tsx +++ b/ui/src/components/TasksTableContainer.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { connect, ConnectedProps } from "react-redux"; import { makeStyles } from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; @@ -18,6 +18,24 @@ import { queueDetailsPath, taskDetailsPath } from "../paths"; import { QueueInfo } from "../reducers/queuesReducer"; import { AppState } from "../store"; import { isDarkTheme } from "../theme"; +import { Divider } from "@material-ui/core"; +import FilterListIcon from "@material-ui/icons/FilterList"; +import TasksFilterDialog from "./TasksFilterDialog"; +import { + listActiveTasks, + listArchivedTasks, + listCompletedTasks, + listPendingTasks, + listRetryTasks, + listScheduledTasks, + ListTasksResponse, + PaginationOptions, +} from "../api"; +import { + cancelFilterTasks, + filterTasksAsync, + TasksFilter, +} from "../actions/tasksActions"; interface TabPanelProps { children?: React.ReactNode; @@ -65,10 +83,18 @@ function mapStatetoProps(state: AppState, ownProps: Props) { failed: 0, timestamp: "n/a", }; - return { currentStats }; + const filterOp = state.tasks.filterOp; + const filterActive = filterOp != null; + const filterCount = filterOp?.done === true ? filterOp?.result?.length : null; + return { currentStats, filterActive, filterCount }; } -const connector = connect(mapStatetoProps); +const mapDispatchToProps = { + filterTasksAsync, + cancelFilterTasks, +}; + +const connector = connect(mapStatetoProps, mapDispatchToProps); type ReduxProps = ConnectedProps; @@ -146,24 +172,92 @@ const useStyles = makeStyles((theme) => ({ })); function TasksTableContainer(props: Props & ReduxProps) { - const { currentStats } = props; + const { currentStats, cancelFilterTasks } = props; const classes = useStyles(); const history = useHistory(); const chips = [ - { key: "active", label: "Active", count: currentStats.active }, - { key: "pending", label: "Pending", count: currentStats.pending }, + { + key: "active", + label: "Active", + count: currentStats.active, + fetcher: listActiveTasks, + }, + { + key: "pending", + label: "Pending", + count: currentStats.pending, + fetcher: listPendingTasks, + }, { key: "aggregating", label: "Aggregating", count: currentStats.aggregating, + fetcher: null, // TODO Support aggregating table + }, + { + key: "scheduled", + label: "Scheduled", + count: currentStats.scheduled, + fetcher: listScheduledTasks, + }, + { + key: "retry", + label: "Retry", + count: currentStats.retry, + fetcher: listRetryTasks, + }, + { + key: "archived", + label: "Archived", + count: currentStats.archived, + fetcher: listArchivedTasks, + }, + { + key: "completed", + label: "Completed", + count: currentStats.completed, + fetcher: listCompletedTasks, }, - { 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(""); + const [filterModalOpen, setFilterModalOpen] = useState(false); + + useEffect(() => { + // Clear filter when the user selects a different task status + cancelFilterTasks(); + return () => { + // Clear filter when leaving the page + cancelFilterTasks(); + }; + }, [props.selected, cancelFilterTasks]); + + const taskPageFetcher = ( + fetcherApi: ( + qname: string, + pageOpts?: PaginationOptions + ) => Promise + ) => { + const qname = props.queue; + return async (page?: number) => { + return fetcherApi(qname, { + page, + size: 10_000, // TODO Choose a value here intelligently? + }); + }; + }; + + const dispatchFilterAction = (filter: TasksFilter) => { + const fetcherApi = chips.find((c) => c.key === props.selected)?.fetcher; + if (fetcherApi == null) { + console.error( + "Failed to find fetcher API for selected task type:", + props.selected + ); + return; + } + props.filterTasksAsync(filter, taskPageFetcher(fetcherApi)); + }; return ( @@ -187,6 +281,25 @@ function TasksTableContainer(props: Props & ReduxProps) { /> ))} + +
+ {chips.find((c) => c.key === props.selected)?.fetcher && ( + } + label="Filter" + onClick={() => setFilterModalOpen(true)} + onDelete={ + props.filterActive ? props.cancelFilterTasks : undefined + } + color={props.filterActive ? "primary" : "default"} + /> + )} + setFilterModalOpen(false)} + onFilter={dispatchFilterAction} + /> +
@@ -219,13 +332,13 @@ function TasksTableContainer(props: Props & ReduxProps) { @@ -234,25 +347,25 @@ function TasksTableContainer(props: Props & ReduxProps) { diff --git a/ui/src/reducers/filterReducer.ts b/ui/src/reducers/filterReducer.ts new file mode 100644 index 0000000..e3d85ce --- /dev/null +++ b/ui/src/reducers/filterReducer.ts @@ -0,0 +1,149 @@ +import { + ARCHIVE_AGGREGATING_TASK_SUCCESS, + ARCHIVE_PENDING_TASK_SUCCESS, + ARCHIVE_RETRY_TASK_SUCCESS, + ARCHIVE_SCHEDULED_TASK_SUCCESS, + BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS, + BATCH_ARCHIVE_PENDING_TASKS_SUCCESS, + BATCH_ARCHIVE_RETRY_TASKS_SUCCESS, + BATCH_ARCHIVE_SCHEDULED_TASKS_SUCCESS, + BATCH_DELETE_AGGREGATING_TASKS_SUCCESS, + BATCH_DELETE_ARCHIVED_TASKS_SUCCESS, + BATCH_DELETE_COMPLETED_TASKS_SUCCESS, + BATCH_DELETE_PENDING_TASKS_SUCCESS, + BATCH_DELETE_RETRY_TASKS_SUCCESS, + BATCH_DELETE_SCHEDULED_TASKS_SUCCESS, + BATCH_RUN_AGGREGATING_TASKS_SUCCESS, + BATCH_RUN_ARCHIVED_TASKS_SUCCESS, + BATCH_RUN_RETRY_TASKS_SUCCESS, + BATCH_RUN_SCHEDULED_TASKS_SUCCESS, + DELETE_AGGREGATING_TASK_SUCCESS, + DELETE_ARCHIVED_TASK_SUCCESS, + DELETE_COMPLETED_TASK_SUCCESS, + DELETE_PENDING_TASK_SUCCESS, + DELETE_RETRY_TASK_SUCCESS, + DELETE_SCHEDULED_TASK_SUCCESS, + FILTER_TASKS_BEGIN, + FILTER_TASKS_CANCEL, + FILTER_TASKS_PROGRESS, + FILTER_TASKS_SUCCESS, + RUN_AGGREGATING_TASK_SUCCESS, + RUN_ARCHIVED_TASK_SUCCESS, + RUN_RETRY_TASK_SUCCESS, + RUN_SCHEDULED_TASK_SUCCESS, + TasksActionTypes, +} from "../actions/tasksActions"; +import { TasksState } from "./tasksReducer"; +import { TaskInfo } from "../api"; + +function modifyFilterResults( + state: TasksState, + action: (tasks: TaskInfo[]) => TaskInfo[] +): TasksState { + const filterOp = state.filterOp; + if (filterOp == null || !filterOp.done) return state; + + return { + ...state, + filterOp: { + ...filterOp, + result: action(filterOp.result), + }, + }; +} + +export function filterReducer( + state: TasksState, + action: TasksActionTypes +): TasksState { + switch (action.type) { + case FILTER_TASKS_BEGIN: + return { + ...state, + filterOp: { + done: false, + processedTasks: 0, + result: [], + }, + }; + + case FILTER_TASKS_PROGRESS: { + const filterOp = state.filterOp; + if (filterOp == null) return state; + return { + ...state, + filterOp: { + done: false, + processedTasks: filterOp.processedTasks + action.processedTasks, + result: filterOp.result.concat(action.filterResults), + }, + }; + } + + case FILTER_TASKS_SUCCESS: { + const filterOp = state.filterOp; + if (filterOp == null) return state; + return { + ...state, + filterOp: { + ...filterOp, + done: true, + }, + }; + } + + case FILTER_TASKS_CANCEL: { + const { filterOp, ...newState } = state; + return newState; + } + + // Replicate actions made to queues to filter results + + case ARCHIVE_PENDING_TASK_SUCCESS: + case DELETE_PENDING_TASK_SUCCESS: + case DELETE_COMPLETED_TASK_SUCCESS: + case RUN_SCHEDULED_TASK_SUCCESS: + case ARCHIVE_SCHEDULED_TASK_SUCCESS: + case DELETE_SCHEDULED_TASK_SUCCESS: + case RUN_AGGREGATING_TASK_SUCCESS: + case ARCHIVE_AGGREGATING_TASK_SUCCESS: + case DELETE_AGGREGATING_TASK_SUCCESS: + case RUN_RETRY_TASK_SUCCESS: + case ARCHIVE_RETRY_TASK_SUCCESS: + case DELETE_RETRY_TASK_SUCCESS: + case RUN_ARCHIVED_TASK_SUCCESS: + case DELETE_ARCHIVED_TASK_SUCCESS: + return modifyFilterResults(state, (tasks) => + tasks.filter((task) => task.id !== action.taskId) + ); + + case BATCH_ARCHIVE_PENDING_TASKS_SUCCESS: + case BATCH_ARCHIVE_SCHEDULED_TASKS_SUCCESS: + case BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS: + case BATCH_ARCHIVE_RETRY_TASKS_SUCCESS: + return modifyFilterResults(state, (tasks) => + tasks.filter((task) => !action.payload.archived_ids.includes(task.id)) + ); + + case BATCH_DELETE_COMPLETED_TASKS_SUCCESS: + case BATCH_DELETE_PENDING_TASKS_SUCCESS: + case BATCH_DELETE_SCHEDULED_TASKS_SUCCESS: + case BATCH_DELETE_AGGREGATING_TASKS_SUCCESS: + case BATCH_DELETE_RETRY_TASKS_SUCCESS: + case BATCH_DELETE_ARCHIVED_TASKS_SUCCESS: + return modifyFilterResults(state, (tasks) => + tasks.filter((task) => !action.payload.deleted_ids.includes(task.id)) + ); + + case BATCH_RUN_SCHEDULED_TASKS_SUCCESS: + case BATCH_RUN_AGGREGATING_TASKS_SUCCESS: + case BATCH_RUN_RETRY_TASKS_SUCCESS: + case BATCH_RUN_ARCHIVED_TASKS_SUCCESS: + return modifyFilterResults(state, (tasks) => + tasks.filter((task) => !action.payload.pending_ids.includes(task.id)) + ); + + default: + return state; + } +} diff --git a/ui/src/reducers/queuesReducer.ts b/ui/src/reducers/queuesReducer.ts index ce71a07..307ada4 100644 --- a/ui/src/reducers/queuesReducer.ts +++ b/ui/src/reducers/queuesReducer.ts @@ -3,44 +3,64 @@ import { LIST_GROUPS_SUCCESS, } from "../actions/groupsActions"; import { - LIST_QUEUES_SUCCESS, - LIST_QUEUES_BEGIN, - QueuesActionTypes, - PAUSE_QUEUE_BEGIN, - PAUSE_QUEUE_SUCCESS, - PAUSE_QUEUE_ERROR, - RESUME_QUEUE_BEGIN, - RESUME_QUEUE_ERROR, - RESUME_QUEUE_SUCCESS, DELETE_QUEUE_BEGIN, DELETE_QUEUE_ERROR, DELETE_QUEUE_SUCCESS, + LIST_QUEUES_BEGIN, LIST_QUEUES_ERROR, + LIST_QUEUES_SUCCESS, + PAUSE_QUEUE_BEGIN, + PAUSE_QUEUE_ERROR, + PAUSE_QUEUE_SUCCESS, + QueuesActionTypes, + RESUME_QUEUE_BEGIN, + RESUME_QUEUE_ERROR, + RESUME_QUEUE_SUCCESS, } from "../actions/queuesActions"; import { - BATCH_DELETE_ARCHIVED_TASKS_SUCCESS, - BATCH_DELETE_RETRY_TASKS_SUCCESS, - BATCH_DELETE_SCHEDULED_TASKS_SUCCESS, + ARCHIVE_AGGREGATING_TASK_SUCCESS, + ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS, + ARCHIVE_ALL_PENDING_TASKS_SUCCESS, + ARCHIVE_ALL_RETRY_TASKS_SUCCESS, + ARCHIVE_ALL_SCHEDULED_TASKS_SUCCESS, + ARCHIVE_PENDING_TASK_SUCCESS, + ARCHIVE_RETRY_TASK_SUCCESS, + ARCHIVE_SCHEDULED_TASK_SUCCESS, + BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS, + BATCH_ARCHIVE_PENDING_TASKS_SUCCESS, BATCH_ARCHIVE_RETRY_TASKS_SUCCESS, BATCH_ARCHIVE_SCHEDULED_TASKS_SUCCESS, + BATCH_DELETE_AGGREGATING_TASKS_SUCCESS, + BATCH_DELETE_ARCHIVED_TASKS_SUCCESS, + BATCH_DELETE_COMPLETED_TASKS_SUCCESS, + BATCH_DELETE_PENDING_TASKS_SUCCESS, + BATCH_DELETE_RETRY_TASKS_SUCCESS, + BATCH_DELETE_SCHEDULED_TASKS_SUCCESS, + BATCH_RUN_AGGREGATING_TASKS_SUCCESS, BATCH_RUN_ARCHIVED_TASKS_SUCCESS, BATCH_RUN_RETRY_TASKS_SUCCESS, BATCH_RUN_SCHEDULED_TASKS_SUCCESS, + DELETE_AGGREGATING_TASK_SUCCESS, + DELETE_ALL_AGGREGATING_TASKS_SUCCESS, DELETE_ALL_ARCHIVED_TASKS_SUCCESS, + DELETE_ALL_COMPLETED_TASKS_SUCCESS, + DELETE_ALL_PENDING_TASKS_SUCCESS, DELETE_ALL_RETRY_TASKS_SUCCESS, DELETE_ALL_SCHEDULED_TASKS_SUCCESS, DELETE_ARCHIVED_TASK_SUCCESS, + DELETE_COMPLETED_TASK_SUCCESS, + DELETE_PENDING_TASK_SUCCESS, DELETE_RETRY_TASK_SUCCESS, DELETE_SCHEDULED_TASK_SUCCESS, - ARCHIVE_ALL_RETRY_TASKS_SUCCESS, - ARCHIVE_ALL_SCHEDULED_TASKS_SUCCESS, - ARCHIVE_RETRY_TASK_SUCCESS, - ARCHIVE_SCHEDULED_TASK_SUCCESS, + FILTER_TASKS_PROGRESS, LIST_ACTIVE_TASKS_SUCCESS, + LIST_AGGREGATING_TASKS_SUCCESS, LIST_ARCHIVED_TASKS_SUCCESS, LIST_PENDING_TASKS_SUCCESS, LIST_RETRY_TASKS_SUCCESS, LIST_SCHEDULED_TASKS_SUCCESS, + RUN_AGGREGATING_TASK_SUCCESS, + RUN_ALL_AGGREGATING_TASKS_SUCCESS, RUN_ALL_ARCHIVED_TASKS_SUCCESS, RUN_ALL_RETRY_TASKS_SUCCESS, RUN_ALL_SCHEDULED_TASKS_SUCCESS, @@ -48,25 +68,6 @@ 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, - DELETE_COMPLETED_TASK_SUCCESS, - DELETE_ALL_COMPLETED_TASKS_SUCCESS, - BATCH_DELETE_COMPLETED_TASKS_SUCCESS, - DELETE_ALL_AGGREGATING_TASKS_SUCCESS, - ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS, - RUN_ALL_AGGREGATING_TASKS_SUCCESS, - BATCH_DELETE_AGGREGATING_TASKS_SUCCESS, - BATCH_RUN_AGGREGATING_TASKS_SUCCESS, - BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS, - DELETE_AGGREGATING_TASK_SUCCESS, - RUN_AGGREGATING_TASK_SUCCESS, - ARCHIVE_AGGREGATING_TASK_SUCCESS, - LIST_AGGREGATING_TASKS_SUCCESS, } from "../actions/tasksActions"; import { Queue } from "../api"; @@ -190,6 +191,17 @@ function queuesReducer( return { ...state, data: newData }; } + case FILTER_TASKS_PROGRESS: { + const newData = state.data + .filter((queueInfo) => queueInfo.name !== action.newStats.queue) + .concat({ + name: action.newStats.queue, + currentStats: action.newStats, + requestPending: false, + }); + return { ...state, data: newData }; + } + case RUN_AGGREGATING_TASK_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 07ed131..a54315e 100644 --- a/ui/src/reducers/tasksReducer.ts +++ b/ui/src/reducers/tasksReducer.ts @@ -1,166 +1,167 @@ import { - LIST_ACTIVE_TASKS_BEGIN, - LIST_ACTIVE_TASKS_SUCCESS, - LIST_ACTIVE_TASKS_ERROR, - TasksActionTypes, - LIST_PENDING_TASKS_BEGIN, - LIST_PENDING_TASKS_SUCCESS, - LIST_PENDING_TASKS_ERROR, - LIST_SCHEDULED_TASKS_BEGIN, - LIST_SCHEDULED_TASKS_SUCCESS, - LIST_SCHEDULED_TASKS_ERROR, - LIST_RETRY_TASKS_BEGIN, - LIST_RETRY_TASKS_SUCCESS, - LIST_RETRY_TASKS_ERROR, - 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, - DELETE_RETRY_TASK_BEGIN, - DELETE_RETRY_TASK_SUCCESS, - DELETE_RETRY_TASK_ERROR, - DELETE_SCHEDULED_TASK_BEGIN, - DELETE_SCHEDULED_TASK_SUCCESS, - DELETE_SCHEDULED_TASK_ERROR, - DELETE_ARCHIVED_TASK_BEGIN, - DELETE_ARCHIVED_TASK_SUCCESS, - DELETE_ARCHIVED_TASK_ERROR, - BATCH_DELETE_ARCHIVED_TASKS_BEGIN, - BATCH_DELETE_ARCHIVED_TASKS_SUCCESS, - BATCH_DELETE_ARCHIVED_TASKS_ERROR, - RUN_ARCHIVED_TASK_BEGIN, - RUN_ARCHIVED_TASK_SUCCESS, - RUN_ARCHIVED_TASK_ERROR, - BATCH_RUN_ARCHIVED_TASKS_BEGIN, - BATCH_RUN_ARCHIVED_TASKS_ERROR, - BATCH_RUN_ARCHIVED_TASKS_SUCCESS, - DELETE_ALL_ARCHIVED_TASKS_BEGIN, - DELETE_ALL_ARCHIVED_TASKS_SUCCESS, - DELETE_ALL_ARCHIVED_TASKS_ERROR, - RUN_ALL_ARCHIVED_TASKS_BEGIN, - RUN_ALL_ARCHIVED_TASKS_ERROR, - RUN_ALL_ARCHIVED_TASKS_SUCCESS, - BATCH_DELETE_RETRY_TASKS_ERROR, - BATCH_RUN_RETRY_TASKS_ERROR, - BATCH_DELETE_RETRY_TASKS_SUCCESS, - BATCH_RUN_RETRY_TASKS_SUCCESS, - BATCH_DELETE_RETRY_TASKS_BEGIN, - BATCH_RUN_RETRY_TASKS_BEGIN, - DELETE_ALL_RETRY_TASKS_ERROR, - RUN_ALL_RETRY_TASKS_ERROR, - DELETE_ALL_RETRY_TASKS_SUCCESS, - RUN_ALL_RETRY_TASKS_SUCCESS, - DELETE_ALL_RETRY_TASKS_BEGIN, - RUN_ALL_RETRY_TASKS_BEGIN, - BATCH_DELETE_SCHEDULED_TASKS_ERROR, - BATCH_RUN_SCHEDULED_TASKS_ERROR, - BATCH_DELETE_SCHEDULED_TASKS_SUCCESS, - BATCH_RUN_SCHEDULED_TASKS_SUCCESS, - BATCH_DELETE_SCHEDULED_TASKS_BEGIN, - BATCH_RUN_SCHEDULED_TASKS_BEGIN, - DELETE_ALL_SCHEDULED_TASKS_ERROR, - RUN_ALL_SCHEDULED_TASKS_ERROR, - DELETE_ALL_SCHEDULED_TASKS_SUCCESS, - RUN_ALL_SCHEDULED_TASKS_SUCCESS, - DELETE_ALL_SCHEDULED_TASKS_BEGIN, - RUN_ALL_SCHEDULED_TASKS_BEGIN, - RUN_RETRY_TASK_BEGIN, - RUN_RETRY_TASK_SUCCESS, - RUN_RETRY_TASK_ERROR, - RUN_SCHEDULED_TASK_BEGIN, - RUN_SCHEDULED_TASK_SUCCESS, - RUN_SCHEDULED_TASK_ERROR, - ARCHIVE_SCHEDULED_TASK_BEGIN, - ARCHIVE_SCHEDULED_TASK_SUCCESS, - ARCHIVE_SCHEDULED_TASK_ERROR, + ARCHIVE_AGGREGATING_TASK_BEGIN, + ARCHIVE_AGGREGATING_TASK_ERROR, + ARCHIVE_AGGREGATING_TASK_SUCCESS, + ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN, + ARCHIVE_ALL_AGGREGATING_TASKS_ERROR, + ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS, + ARCHIVE_ALL_PENDING_TASKS_BEGIN, + ARCHIVE_ALL_PENDING_TASKS_ERROR, + ARCHIVE_ALL_PENDING_TASKS_SUCCESS, + ARCHIVE_ALL_RETRY_TASKS_BEGIN, + ARCHIVE_ALL_RETRY_TASKS_ERROR, + ARCHIVE_ALL_RETRY_TASKS_SUCCESS, ARCHIVE_ALL_SCHEDULED_TASKS_BEGIN, - ARCHIVE_ALL_SCHEDULED_TASKS_SUCCESS, ARCHIVE_ALL_SCHEDULED_TASKS_ERROR, + ARCHIVE_ALL_SCHEDULED_TASKS_SUCCESS, + ARCHIVE_PENDING_TASK_BEGIN, + ARCHIVE_PENDING_TASK_ERROR, + ARCHIVE_PENDING_TASK_SUCCESS, + ARCHIVE_RETRY_TASK_BEGIN, + ARCHIVE_RETRY_TASK_ERROR, + ARCHIVE_RETRY_TASK_SUCCESS, + ARCHIVE_SCHEDULED_TASK_BEGIN, + ARCHIVE_SCHEDULED_TASK_ERROR, + ARCHIVE_SCHEDULED_TASK_SUCCESS, + BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN, + BATCH_ARCHIVE_AGGREGATING_TASKS_ERROR, + BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS, + BATCH_ARCHIVE_PENDING_TASKS_BEGIN, + BATCH_ARCHIVE_PENDING_TASKS_ERROR, + BATCH_ARCHIVE_PENDING_TASKS_SUCCESS, + BATCH_ARCHIVE_RETRY_TASKS_BEGIN, + BATCH_ARCHIVE_RETRY_TASKS_ERROR, + BATCH_ARCHIVE_RETRY_TASKS_SUCCESS, BATCH_ARCHIVE_SCHEDULED_TASKS_BEGIN, BATCH_ARCHIVE_SCHEDULED_TASKS_ERROR, BATCH_ARCHIVE_SCHEDULED_TASKS_SUCCESS, - ARCHIVE_RETRY_TASK_BEGIN, - ARCHIVE_RETRY_TASK_SUCCESS, - ARCHIVE_RETRY_TASK_ERROR, - ARCHIVE_ALL_RETRY_TASKS_BEGIN, - ARCHIVE_ALL_RETRY_TASKS_SUCCESS, - ARCHIVE_ALL_RETRY_TASKS_ERROR, - BATCH_ARCHIVE_RETRY_TASKS_SUCCESS, - BATCH_ARCHIVE_RETRY_TASKS_BEGIN, - BATCH_ARCHIVE_RETRY_TASKS_ERROR, BATCH_CANCEL_ACTIVE_TASKS_BEGIN, - BATCH_CANCEL_ACTIVE_TASKS_SUCCESS, BATCH_CANCEL_ACTIVE_TASKS_ERROR, - CANCEL_ALL_ACTIVE_TASKS_BEGIN, - CANCEL_ALL_ACTIVE_TASKS_SUCCESS, - CANCEL_ALL_ACTIVE_TASKS_ERROR, - ARCHIVE_PENDING_TASK_BEGIN, - DELETE_PENDING_TASK_BEGIN, - ARCHIVE_PENDING_TASK_SUCCESS, - DELETE_PENDING_TASK_SUCCESS, - ARCHIVE_PENDING_TASK_ERROR, - DELETE_PENDING_TASK_ERROR, - ARCHIVE_ALL_PENDING_TASKS_BEGIN, - DELETE_ALL_PENDING_TASKS_BEGIN, - ARCHIVE_ALL_PENDING_TASKS_SUCCESS, - DELETE_ALL_PENDING_TASKS_SUCCESS, - ARCHIVE_ALL_PENDING_TASKS_ERROR, - DELETE_ALL_PENDING_TASKS_ERROR, - BATCH_ARCHIVE_PENDING_TASKS_BEGIN, - BATCH_DELETE_PENDING_TASKS_BEGIN, - BATCH_ARCHIVE_PENDING_TASKS_SUCCESS, - BATCH_DELETE_PENDING_TASKS_SUCCESS, - BATCH_ARCHIVE_PENDING_TASKS_ERROR, - BATCH_DELETE_PENDING_TASKS_ERROR, - 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_CANCEL_ACTIVE_TASKS_SUCCESS, + BATCH_DELETE_AGGREGATING_TASKS_BEGIN, + BATCH_DELETE_AGGREGATING_TASKS_ERROR, + BATCH_DELETE_AGGREGATING_TASKS_SUCCESS, + BATCH_DELETE_ARCHIVED_TASKS_BEGIN, + BATCH_DELETE_ARCHIVED_TASKS_ERROR, + BATCH_DELETE_ARCHIVED_TASKS_SUCCESS, BATCH_DELETE_COMPLETED_TASKS_BEGIN, BATCH_DELETE_COMPLETED_TASKS_ERROR, BATCH_DELETE_COMPLETED_TASKS_SUCCESS, - LIST_AGGREGATING_TASKS_BEGIN, - LIST_AGGREGATING_TASKS_SUCCESS, - LIST_AGGREGATING_TASKS_ERROR, - DELETE_ALL_AGGREGATING_TASKS_BEGIN, - DELETE_ALL_AGGREGATING_TASKS_SUCCESS, - DELETE_ALL_AGGREGATING_TASKS_ERROR, - ARCHIVE_ALL_AGGREGATING_TASKS_BEGIN, - ARCHIVE_ALL_AGGREGATING_TASKS_SUCCESS, - ARCHIVE_ALL_AGGREGATING_TASKS_ERROR, - RUN_ALL_AGGREGATING_TASKS_BEGIN, - RUN_ALL_AGGREGATING_TASKS_SUCCESS, - RUN_ALL_AGGREGATING_TASKS_ERROR, - BATCH_ARCHIVE_AGGREGATING_TASKS_BEGIN, + BATCH_DELETE_PENDING_TASKS_BEGIN, + BATCH_DELETE_PENDING_TASKS_ERROR, + BATCH_DELETE_PENDING_TASKS_SUCCESS, + BATCH_DELETE_RETRY_TASKS_BEGIN, + BATCH_DELETE_RETRY_TASKS_ERROR, + BATCH_DELETE_RETRY_TASKS_SUCCESS, + BATCH_DELETE_SCHEDULED_TASKS_BEGIN, + BATCH_DELETE_SCHEDULED_TASKS_ERROR, + BATCH_DELETE_SCHEDULED_TASKS_SUCCESS, BATCH_RUN_AGGREGATING_TASKS_BEGIN, - BATCH_DELETE_AGGREGATING_TASKS_BEGIN, BATCH_RUN_AGGREGATING_TASKS_ERROR, - BATCH_ARCHIVE_AGGREGATING_TASKS_ERROR, - BATCH_DELETE_AGGREGATING_TASKS_ERROR, - BATCH_DELETE_AGGREGATING_TASKS_SUCCESS, BATCH_RUN_AGGREGATING_TASKS_SUCCESS, - BATCH_ARCHIVE_AGGREGATING_TASKS_SUCCESS, - RUN_AGGREGATING_TASK_BEGIN, - ARCHIVE_AGGREGATING_TASK_BEGIN, + BATCH_RUN_ARCHIVED_TASKS_BEGIN, + BATCH_RUN_ARCHIVED_TASKS_ERROR, + BATCH_RUN_ARCHIVED_TASKS_SUCCESS, + BATCH_RUN_RETRY_TASKS_BEGIN, + BATCH_RUN_RETRY_TASKS_ERROR, + BATCH_RUN_RETRY_TASKS_SUCCESS, + BATCH_RUN_SCHEDULED_TASKS_BEGIN, + BATCH_RUN_SCHEDULED_TASKS_ERROR, + BATCH_RUN_SCHEDULED_TASKS_SUCCESS, + CANCEL_ACTIVE_TASK_BEGIN, + CANCEL_ACTIVE_TASK_ERROR, + CANCEL_ACTIVE_TASK_SUCCESS, + CANCEL_ALL_ACTIVE_TASKS_BEGIN, + CANCEL_ALL_ACTIVE_TASKS_ERROR, + CANCEL_ALL_ACTIVE_TASKS_SUCCESS, DELETE_AGGREGATING_TASK_BEGIN, - RUN_AGGREGATING_TASK_ERROR, - ARCHIVE_AGGREGATING_TASK_ERROR, DELETE_AGGREGATING_TASK_ERROR, - RUN_AGGREGATING_TASK_SUCCESS, - ARCHIVE_AGGREGATING_TASK_SUCCESS, DELETE_AGGREGATING_TASK_SUCCESS, + DELETE_ALL_AGGREGATING_TASKS_BEGIN, + DELETE_ALL_AGGREGATING_TASKS_ERROR, + DELETE_ALL_AGGREGATING_TASKS_SUCCESS, + DELETE_ALL_ARCHIVED_TASKS_BEGIN, + DELETE_ALL_ARCHIVED_TASKS_ERROR, + DELETE_ALL_ARCHIVED_TASKS_SUCCESS, + DELETE_ALL_COMPLETED_TASKS_BEGIN, + DELETE_ALL_COMPLETED_TASKS_ERROR, + DELETE_ALL_COMPLETED_TASKS_SUCCESS, + DELETE_ALL_PENDING_TASKS_BEGIN, + DELETE_ALL_PENDING_TASKS_ERROR, + DELETE_ALL_PENDING_TASKS_SUCCESS, + DELETE_ALL_RETRY_TASKS_BEGIN, + DELETE_ALL_RETRY_TASKS_ERROR, + DELETE_ALL_RETRY_TASKS_SUCCESS, + DELETE_ALL_SCHEDULED_TASKS_BEGIN, + DELETE_ALL_SCHEDULED_TASKS_ERROR, + DELETE_ALL_SCHEDULED_TASKS_SUCCESS, + DELETE_ARCHIVED_TASK_BEGIN, + DELETE_ARCHIVED_TASK_ERROR, + DELETE_ARCHIVED_TASK_SUCCESS, + DELETE_COMPLETED_TASK_BEGIN, + DELETE_COMPLETED_TASK_ERROR, + DELETE_COMPLETED_TASK_SUCCESS, + DELETE_PENDING_TASK_BEGIN, + DELETE_PENDING_TASK_ERROR, + DELETE_PENDING_TASK_SUCCESS, + DELETE_RETRY_TASK_BEGIN, + DELETE_RETRY_TASK_ERROR, + DELETE_RETRY_TASK_SUCCESS, + DELETE_SCHEDULED_TASK_BEGIN, + DELETE_SCHEDULED_TASK_ERROR, + DELETE_SCHEDULED_TASK_SUCCESS, + GET_TASK_INFO_BEGIN, + GET_TASK_INFO_ERROR, + GET_TASK_INFO_SUCCESS, + LIST_ACTIVE_TASKS_BEGIN, + LIST_ACTIVE_TASKS_ERROR, + LIST_ACTIVE_TASKS_SUCCESS, + LIST_AGGREGATING_TASKS_BEGIN, + LIST_AGGREGATING_TASKS_ERROR, + LIST_AGGREGATING_TASKS_SUCCESS, + LIST_ARCHIVED_TASKS_BEGIN, + LIST_ARCHIVED_TASKS_ERROR, + LIST_ARCHIVED_TASKS_SUCCESS, + LIST_COMPLETED_TASKS_BEGIN, + LIST_COMPLETED_TASKS_ERROR, + LIST_COMPLETED_TASKS_SUCCESS, + LIST_PENDING_TASKS_BEGIN, + LIST_PENDING_TASKS_ERROR, + LIST_PENDING_TASKS_SUCCESS, + LIST_RETRY_TASKS_BEGIN, + LIST_RETRY_TASKS_ERROR, + LIST_RETRY_TASKS_SUCCESS, + LIST_SCHEDULED_TASKS_BEGIN, + LIST_SCHEDULED_TASKS_ERROR, + LIST_SCHEDULED_TASKS_SUCCESS, + RUN_AGGREGATING_TASK_BEGIN, + RUN_AGGREGATING_TASK_ERROR, + RUN_AGGREGATING_TASK_SUCCESS, + RUN_ALL_AGGREGATING_TASKS_BEGIN, + RUN_ALL_AGGREGATING_TASKS_ERROR, + RUN_ALL_AGGREGATING_TASKS_SUCCESS, + RUN_ALL_ARCHIVED_TASKS_BEGIN, + RUN_ALL_ARCHIVED_TASKS_ERROR, + RUN_ALL_ARCHIVED_TASKS_SUCCESS, + RUN_ALL_RETRY_TASKS_BEGIN, + RUN_ALL_RETRY_TASKS_ERROR, + RUN_ALL_RETRY_TASKS_SUCCESS, + RUN_ALL_SCHEDULED_TASKS_BEGIN, + RUN_ALL_SCHEDULED_TASKS_ERROR, + RUN_ALL_SCHEDULED_TASKS_SUCCESS, + RUN_ARCHIVED_TASK_BEGIN, + RUN_ARCHIVED_TASK_ERROR, + RUN_ARCHIVED_TASK_SUCCESS, + RUN_RETRY_TASK_BEGIN, + RUN_RETRY_TASK_ERROR, + RUN_RETRY_TASK_SUCCESS, + RUN_SCHEDULED_TASK_BEGIN, + RUN_SCHEDULED_TASK_ERROR, + RUN_SCHEDULED_TASK_SUCCESS, + TasksActionTypes, } from "../actions/tasksActions"; import { TaskInfo } from "../api"; +import { filterReducer } from "./filterReducer"; export interface TaskInfoExtended extends TaskInfo { // Indicates that a request has been sent for this @@ -174,7 +175,7 @@ export interface TaskInfoExtended extends TaskInfo { canceling?: boolean; } -interface TasksState { +export interface TasksState { activeTasks: { loading: boolean; batchActionPending: boolean; @@ -230,6 +231,12 @@ interface TasksState { error: string; data?: TaskInfo; }; + filterOp?: { + done: boolean; + processedTasks: number; + result: TaskInfo[]; + error?: string; + }; } const initialState: TasksState = { @@ -293,6 +300,7 @@ function tasksReducer( state = initialState, action: TasksActionTypes ): TasksState { + state = filterReducer(state, action); switch (action.type) { case GET_TASK_INFO_BEGIN: return { diff --git a/ui/src/store.ts b/ui/src/store.ts index 96a3ddb..b9628fc 100644 --- a/ui/src/store.ts +++ b/ui/src/store.ts @@ -32,6 +32,12 @@ export type AppState = ReturnType; const store = configureStore({ reducer: rootReducer, preloadedState, + middleware: (getDefaultMiddleware) => + // Disable debug middleware for tasks.filterOp as it may contain large state + getDefaultMiddleware({ + immutableCheck: { ignoredPaths: ["tasks.filterOp"] }, + serializableCheck: { ignoredPaths: ["tasks.filterOp"] }, + }), }); export default store;