From e9b91c4e03654d5968f8a676a13fc4badf530b5d Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Sat, 5 Jul 2025 11:52:13 +0800 Subject: [PATCH] feat(dashboard): cleanup tasks and events (#2368) --- public/locales/en-US/dashboard.json | 13 ++ public/locales/ja-JP/dashboard.json | 13 ++ public/locales/zh-CN/dashboard.json | 14 +- public/locales/zh-TW/dashboard.json | 13 ++ src/api/api.ts | 15 +++ src/api/dashboard.ts | 8 +- .../Admin/Task/TaskCleanupDialog.tsx | 124 ++++++++++++++++++ src/component/Admin/Task/TaskList.tsx | 12 ++ .../Admin/Task/TaskStatusSelector.tsx | 71 ++++++++++ src/component/Admin/Task/TaskTypeSelector.tsx | 70 ++++++++++ 10 files changed, 351 insertions(+), 2 deletions(-) create mode 100644 src/component/Admin/Task/TaskCleanupDialog.tsx create mode 100644 src/component/Admin/Task/TaskStatusSelector.tsx create mode 100644 src/component/Admin/Task/TaskTypeSelector.tsx diff --git a/public/locales/en-US/dashboard.json b/public/locales/en-US/dashboard.json index 9d6e171..52459cb 100644 --- a/public/locales/en-US/dashboard.json +++ b/public/locales/en-US/dashboard.json @@ -1267,6 +1267,12 @@ "forceDeleteDes": "Whether to delete the Blob record regardless of whether the physical file is deleted." }, "event": { + "cleanup": "Cleanup", + "cleanupAuditLog": "Event cleanup", + "cleanupAuditLogDescription": "Delete all events that meet the following conditions:", + "cleanupNotAfter": "Before this date", + "cleanupEventTypes": "Event types", + "cleanupEventTypesDes": "Select the event types to clean up. Leave blank to clean up all types.", "initiator": "Initiator", "event": "Event", "userID": "User ID", @@ -1319,6 +1325,13 @@ "file": "File" }, "task": { + "cleanupTasks": "Cleanup tasks", + "cleanupTasksDescription": "Cleanup all tasks that meet the following conditions:", + "cleanupNotAfter": "Before this date", + "cleanupTaskTypes": "Task types", + "cleanupTaskTypesDes": "Select the task types to clean up. Leave blank to clean up all types.", + "cleanupTaskStatuses": "Task statuses", + "cleanupTaskStatusesDes": "Select the task statuses to clean up. Leave blank to clean up all completed status tasks.", "confirmDelete": "Are you sure you want to delete this task?", "confirmBatchDelete": "Are you sure you want to delete {{num}} tasks?", "deleteXTasks": "Delete {{num}} tasks", diff --git a/public/locales/ja-JP/dashboard.json b/public/locales/ja-JP/dashboard.json index 2140726..1a4706a 100644 --- a/public/locales/ja-JP/dashboard.json +++ b/public/locales/ja-JP/dashboard.json @@ -1253,6 +1253,12 @@ "forceDeleteDes": "物理ファイルの削除成功可否に関わらず、Blobレコードは削除されます。" }, "event": { + "cleanup": "イベント清理", + "cleanupAuditLog": "イベント清理", + "cleanupAuditLogDescription": "以下条件を満たすすべてのイベントを削除します:", + "cleanupNotAfter": "この日付以前", + "cleanupEventTypes": "イベント種類", + "cleanupEventTypesDes": "イベント種類を選択してください。空の場合はすべての種類を削除します。", "initiator": "発信者", "event": "イベント", "userID": "ユーザーID", @@ -1305,6 +1311,13 @@ "file": "ファイル" }, "task": { + "cleanupTasks": "タスク清理", + "cleanupTasksDescription": "以下条件を満たすすべてのタスクを削除します:", + "cleanupNotAfter": "この日付以前", + "cleanupTaskTypes": "タスク種類", + "cleanupTaskTypesDes": "タスク種類を選択してください。空の場合はすべての種類を削除します。", + "cleanupTaskStatuses": "タスクステータス", + "cleanupTaskStatusesDes": "タスクステータスを選択してください。空の場合はすべての完了ステータスのタスクを削除します。", "import": "ファイルのインポート", "confirmDelete": "このタスクを削除しますか?", "confirmBatchDelete": "{{num}}個のタスクを削除しますか?", diff --git a/public/locales/zh-CN/dashboard.json b/public/locales/zh-CN/dashboard.json index 69f18de..7d66631 100644 --- a/public/locales/zh-CN/dashboard.json +++ b/public/locales/zh-CN/dashboard.json @@ -1253,7 +1253,12 @@ "forceDeleteDes": "无论物理文件是否删除成功,都会删除 Blob 记录。" }, "event": { - "initiator": "发起者", + "cleanup": "清理", + "cleanupAuditLog": "清理事件", + "cleanupAuditLogDescription": "删除满足以下条件的所有事件:", + "cleanupNotAfter": "在此日期之前", + "cleanupEventTypes": "事件类型", + "cleanupEventTypesDes": "选择要清理的事件类型,留空表示清理所有类型。", "event": "事件", "userID": "用户 ID", "ip": "IP", @@ -1305,6 +1310,13 @@ "file": "文件" }, "task": { + "cleanupTasks": "清理任务", + "cleanupTasksDescription": "清理满足以下条件的所有任务:", + "cleanupNotAfter": "在此日期之前", + "cleanupTaskTypes": "任务类型", + "cleanupTaskTypesDes": "选择要清理的任务类型,留空表示清理所有类型。", + "cleanupTaskStatuses": "任务状态", + "cleanupTaskStatusesDes": "选择要清理的任务状态,留空表示清理所有已完成状态的任务。", "import": "导入文件", "confirmDelete": "确认要删除这个任务?", "confirmBatchDelete": "确认要删除 {{num}} 个任务?", diff --git a/public/locales/zh-TW/dashboard.json b/public/locales/zh-TW/dashboard.json index 1e9308d..239578d 100644 --- a/public/locales/zh-TW/dashboard.json +++ b/public/locales/zh-TW/dashboard.json @@ -1249,6 +1249,12 @@ "forceDeleteDes": "無論物理檔案是否刪除成功,都會刪除 Blob 記錄。" }, "event": { + "cleanup": "清理", + "cleanupAuditLog": "事件清理", + "cleanupAuditLogDescription": "刪除滿足以下條件的所有事件:", + "cleanupNotAfter": "在此日期之前", + "cleanupEventTypes": "事件類型", + "cleanupEventTypesDes": "選擇要清理的事件類型,留空表示清理所有類型。", "initiator": "發起者", "event": "事件", "userID": "使用者 ID", @@ -1301,6 +1307,13 @@ "file": "檔案" }, "task": { + "cleanupTasks": "清理任務", + "cleanupTasksDescription": "清理滿足以下條件的所有任務:", + "cleanupNotAfter": "在此日期之前", + "cleanupTaskTypes": "任務類型", + "cleanupTaskTypesDes": "選擇要清理的任務類型,留空表示清理所有類型。", + "cleanupTaskStatuses": "任務狀態", + "cleanupTaskStatusesDes": "選擇要清理的任務狀態,留空表示清理所有已完成狀態的任務。", "import": "導入檔案", "confirmDelete": "確認要刪除這個任務?", "confirmBatchDelete": "確認要刪除 {{num}} 個任務?", diff --git a/src/api/api.ts b/src/api/api.ts index 32deb9c..cd4bbc4 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -4,6 +4,7 @@ import { AdminListGroupResponse, AdminListService, BatchIDService, + CleanupTaskService, CreateStoragePolicyCorsService, Entity, FetchWOPIDiscoveryService, @@ -1992,3 +1993,17 @@ export function sendPatchViewSync(args: PatchViewSyncService): ThunkResponse { + return async (dispatch, _getState) => { + return await dispatch( + send( + `/admin/queue/cleanup`, + { method: "POST", data: args }, + { + ...defaultOpts, + }, + ), + ); + }; +} diff --git a/src/api/dashboard.ts b/src/api/dashboard.ts index af14b98..ece643f 100644 --- a/src/api/dashboard.ts +++ b/src/api/dashboard.ts @@ -1,6 +1,6 @@ import { EntityType, PaginationResults, PolicyType } from "./explorer.ts"; import { Capacity } from "./user.ts"; -import { TaskStatus, TaskSummary } from "./workflow.ts"; +import { TaskStatus, TaskSummary, TaskType } from "./workflow.ts"; export interface MetricsSummary { dates: string[]; @@ -541,3 +541,9 @@ export interface ListShareResponse { shares: Share[]; pagination: PaginationResults; } + +export interface CleanupTaskService { + not_after: string; + types?: TaskType[]; + status?: TaskStatus[]; +} diff --git a/src/component/Admin/Task/TaskCleanupDialog.tsx b/src/component/Admin/Task/TaskCleanupDialog.tsx new file mode 100644 index 0000000..b449f2a --- /dev/null +++ b/src/component/Admin/Task/TaskCleanupDialog.tsx @@ -0,0 +1,124 @@ +import { Button, DialogContent, SelectChangeEvent, Stack, Typography } from "@mui/material"; +import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; +import dayjs from "dayjs"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { sendCleanupTask } from "../../../api/api"; +import { CleanupTaskService } from "../../../api/dashboard"; +import { TaskStatus, TaskType } from "../../../api/workflow"; +import { useAppDispatch } from "../../../redux/hooks"; +import DraggableDialog from "../../Dialogs/DraggableDialog"; +import SettingForm from "../../Pages/Setting/SettingForm"; +import TaskStatusSelector from "./TaskStatusSelector"; +import TaskTypeSelector from "./TaskTypeSelector"; + +export interface TaskCleanupDialogProps { + open: boolean; + onClose: () => void; + onCleanupComplete?: () => void; +} + +const TaskCleanupDialog = ({ open, onClose, onCleanupComplete }: TaskCleanupDialogProps) => { + const { t } = useTranslation("dashboard"); + const dispatch = useAppDispatch(); + const [notAfter, setNotAfter] = useState(null); + const [selectedTypes, setSelectedTypes] = useState([]); + const [selectedStatuses, setSelectedStatuses] = useState([]); + const [loading, setLoading] = useState(false); + + const handleCleanup = async () => { + if (!notAfter) { + return; + } + + setLoading(true); + try { + const args: CleanupTaskService = { + not_after: notAfter.toISOString(), + types: selectedTypes.length > 0 ? selectedTypes : undefined, + status: selectedStatuses.length > 0 ? selectedStatuses : undefined, + }; + + await dispatch(sendCleanupTask(args)); + onCleanupComplete?.(); + onClose(); + } finally { + setLoading(false); + } + }; + + const handleReset = () => { + setNotAfter(null); + setSelectedTypes([]); + setSelectedStatuses([]); + }; + + return ( + + {t("user.reset")} + + } + > + + + + {t("task.cleanupTasksDescription")} + + + + + setNotAfter(newValue)} + slotProps={{ + textField: { + fullWidth: true, + size: "small", + }, + }} + /> + + + + + ) => setSelectedTypes(e.target.value as TaskType[])} + helperText={t("task.cleanupTaskTypesDes")} + showAllOption={false} + displayEmpty={true} + /> + + + + ) => setSelectedStatuses(e.target.value as TaskStatus[])} + helperText={t("task.cleanupTaskStatusesDes")} + showAllOption={false} + displayEmpty={true} + /> + + + + + ); +}; + +export default TaskCleanupDialog; diff --git a/src/component/Admin/Task/TaskList.tsx b/src/component/Admin/Task/TaskList.tsx index f37b032..ebd7031 100644 --- a/src/component/Admin/Task/TaskList.tsx +++ b/src/component/Admin/Task/TaskList.tsx @@ -27,6 +27,7 @@ import { useAppDispatch } from "../../../redux/hooks"; import { confirmOperation } from "../../../redux/thunks/dialog"; import { NoWrapTableCell, SecondaryButton, StyledTableContainerPaper } from "../../Common/StyledComponents"; import ArrowSync from "../../Icons/ArrowSync"; +import Broom from "../../Icons/Broom"; import Filter from "../../Icons/Filter"; import PageContainer from "../../Pages/PageContainer"; import PageHeader from "../../Pages/PageHeader"; @@ -34,6 +35,7 @@ import TablePagination from "../Common/TablePagination"; import EntityDialog from "../Entity/EntityDialog/EntityDialog"; import { OrderByQuery, OrderDirectionQuery, PageQuery, PageSizeQuery } from "../StoragePolicy/StoragePolicySetting"; import UserDialog from "../User/UserDialog/UserDialog"; +import TaskCleanupDialog from "./TaskCleanupDialog"; import TaskDialog from "./TaskDialog/TaskDialog"; import TaskFilterPopover from "./TaskFilterPopover"; import TaskRow from "./TaskRow"; @@ -77,6 +79,7 @@ const TaskList = () => { const [openTask, setOpenTask] = useState(undefined); const [openTaskDialogOpen, setOpenTaskDialogOpen] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); + const [cleanupDialogOpen, setCleanupDialogOpen] = useState(false); const pageInt = parseInt(page) ?? 1; const pageSizeInt = parseInt(pageSize) ?? 10; @@ -199,6 +202,11 @@ const TaskList = () => { setUserDialogOpen(false)} userID={userDialogID} /> setOpenEntityDialogOpen(false)} entityID={openEntity} /> setOpenTaskDialogOpen(false)} taskID={openTask} /> + setCleanupDialogOpen(false)} + onCleanupComplete={fetchTasks} + /> @@ -225,6 +233,10 @@ const TaskList = () => { + } variant="contained" onClick={() => setCleanupDialogOpen(true)}> + {t("event.cleanup")} + + {selected.length > 0 && !isMobile && ( <> diff --git a/src/component/Admin/Task/TaskStatusSelector.tsx b/src/component/Admin/Task/TaskStatusSelector.tsx new file mode 100644 index 0000000..742a31c --- /dev/null +++ b/src/component/Admin/Task/TaskStatusSelector.tsx @@ -0,0 +1,71 @@ +import { Box, FormHelperText, ListItemText, SelectChangeEvent } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { TaskStatus } from "../../../api/workflow"; +import { DenseSelect, SquareChip } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; +import { getTaskStatusText } from "../../Pages/Tasks/TaskProps"; + +interface TaskStatusSelectorProps { + value: TaskStatus[]; + onChange: (event: SelectChangeEvent) => void; + renderValue?: (selected: unknown) => React.ReactNode; + helperText?: string; + showAllOption?: boolean; + allOptionText?: string; + fullWidth?: boolean; + displayEmpty?: boolean; +} + +const TaskStatusSelector = ({ + value, + onChange, + renderValue, + helperText, + showAllOption = false, + allOptionText, + fullWidth = true, + displayEmpty = false, +}: TaskStatusSelectorProps) => { + const { t } = useTranslation("dashboard"); + + const defaultRenderValue = (selected: unknown) => { + const values = Array.isArray(selected) ? selected : []; + return ( + + {values.map((val) => ( + + ))} + + ); + }; + + return ( + <> + + {showAllOption && ( + + + + )} + {Object.values(TaskStatus).map((status) => ( + + + + ))} + + {helperText && {helperText}} + + ); +}; + +export default TaskStatusSelector; diff --git a/src/component/Admin/Task/TaskTypeSelector.tsx b/src/component/Admin/Task/TaskTypeSelector.tsx new file mode 100644 index 0000000..e84508a --- /dev/null +++ b/src/component/Admin/Task/TaskTypeSelector.tsx @@ -0,0 +1,70 @@ +import { Box, FormHelperText, ListItemText, SelectChangeEvent } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { TaskType } from "../../../api/workflow"; +import { DenseSelect, SquareChip } from "../../Common/StyledComponents"; +import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu"; + +interface TaskTypeSelectorProps { + value: TaskType[]; + onChange: (event: SelectChangeEvent) => void; + renderValue?: (selected: unknown) => React.ReactNode; + helperText?: string; + showAllOption?: boolean; + allOptionText?: string; + fullWidth?: boolean; + displayEmpty?: boolean; +} + +const TaskTypeSelector = ({ + value, + onChange, + renderValue, + helperText, + showAllOption = false, + allOptionText, + fullWidth = true, + displayEmpty = false, +}: TaskTypeSelectorProps) => { + const { t } = useTranslation("dashboard"); + + const defaultRenderValue = (selected: unknown) => { + const values = Array.isArray(selected) ? selected : []; + return ( + + {values.map((val) => ( + + ))} + + ); + }; + + return ( + <> + + {showAllOption && ( + + + + )} + {Object.values(TaskType).map((type) => ( + + + + ))} + + {helperText && {helperText}} + + ); +}; + +export default TaskTypeSelector;