mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-26 04:02:47 +00:00
feat(dashboard): cleanup tasks and events (#2368)
This commit is contained in:
parent
6a6fd722f3
commit
e9b91c4e03
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}}個のタスクを削除しますか?",
|
||||
|
|
|
|||
|
|
@ -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}} 个任务?",
|
||||
|
|
|
|||
|
|
@ -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}} 個任務?",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import {
|
|||
AdminListGroupResponse,
|
||||
AdminListService,
|
||||
BatchIDService,
|
||||
CleanupTaskService,
|
||||
CreateStoragePolicyCorsService,
|
||||
Entity,
|
||||
FetchWOPIDiscoveryService,
|
||||
|
|
@ -1992,3 +1993,17 @@ export function sendPatchViewSync(args: PatchViewSyncService): ThunkResponse<voi
|
|||
);
|
||||
};
|
||||
}
|
||||
|
||||
export function sendCleanupTask(args: CleanupTaskService): ThunkResponse<void> {
|
||||
return async (dispatch, _getState) => {
|
||||
return await dispatch(
|
||||
send(
|
||||
`/admin/queue/cleanup`,
|
||||
{ method: "POST", data: args },
|
||||
{
|
||||
...defaultOpts,
|
||||
},
|
||||
),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<dayjs.Dayjs | null>(null);
|
||||
const [selectedTypes, setSelectedTypes] = useState<TaskType[]>([]);
|
||||
const [selectedStatuses, setSelectedStatuses] = useState<TaskStatus[]>([]);
|
||||
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 (
|
||||
<DraggableDialog
|
||||
title={t("task.cleanupTasks")}
|
||||
dialogProps={{
|
||||
open,
|
||||
onClose,
|
||||
maxWidth: "sm",
|
||||
fullWidth: true,
|
||||
}}
|
||||
showActions={true}
|
||||
showCancel={true}
|
||||
onAccept={handleCleanup}
|
||||
loading={loading}
|
||||
disabled={!notAfter}
|
||||
okText={t("event.cleanup")}
|
||||
secondaryAction={
|
||||
<Button onClick={handleReset} disabled={loading}>
|
||||
{t("user.reset")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<DialogContent>
|
||||
<Stack spacing={3}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t("task.cleanupTasksDescription")}
|
||||
</Typography>
|
||||
|
||||
<SettingForm title={t("task.cleanupNotAfter")} noContainer lgWidth={12}>
|
||||
<LocalizationProvider dateAdapter={AdapterDayjs}>
|
||||
<DateTimePicker
|
||||
value={notAfter}
|
||||
onChange={(newValue) => setNotAfter(newValue)}
|
||||
slotProps={{
|
||||
textField: {
|
||||
fullWidth: true,
|
||||
size: "small",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</SettingForm>
|
||||
|
||||
<SettingForm title={t("task.type")} noContainer lgWidth={12}>
|
||||
<TaskTypeSelector
|
||||
value={selectedTypes}
|
||||
onChange={(e: SelectChangeEvent<unknown>) => setSelectedTypes(e.target.value as TaskType[])}
|
||||
helperText={t("task.cleanupTaskTypesDes")}
|
||||
showAllOption={false}
|
||||
displayEmpty={true}
|
||||
/>
|
||||
</SettingForm>
|
||||
|
||||
<SettingForm title={t("task.status")} noContainer lgWidth={12}>
|
||||
<TaskStatusSelector
|
||||
value={selectedStatuses}
|
||||
onChange={(e: SelectChangeEvent<unknown>) => setSelectedStatuses(e.target.value as TaskStatus[])}
|
||||
helperText={t("task.cleanupTaskStatusesDes")}
|
||||
showAllOption={false}
|
||||
displayEmpty={true}
|
||||
/>
|
||||
</SettingForm>
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</DraggableDialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskCleanupDialog;
|
||||
|
|
@ -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<number | undefined>(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 = () => {
|
|||
<UserDialog open={userDialogOpen} onClose={() => setUserDialogOpen(false)} userID={userDialogID} />
|
||||
<EntityDialog open={openEntityDialogOpen} onClose={() => setOpenEntityDialogOpen(false)} entityID={openEntity} />
|
||||
<TaskDialog open={openTaskDialogOpen} onClose={() => setOpenTaskDialogOpen(false)} taskID={openTask} />
|
||||
<TaskCleanupDialog
|
||||
open={cleanupDialogOpen}
|
||||
onClose={() => setCleanupDialogOpen(false)}
|
||||
onCleanupComplete={fetchTasks}
|
||||
/>
|
||||
<Container maxWidth="xl">
|
||||
<PageHeader title={t("dashboard:nav.tasks")} />
|
||||
<Stack direction="row" spacing={1} sx={{ mb: 2 }}>
|
||||
|
|
@ -225,6 +233,10 @@ const TaskList = () => {
|
|||
</SecondaryButton>
|
||||
</Badge>
|
||||
|
||||
<SecondaryButton startIcon={<Broom />} variant="contained" onClick={() => setCleanupDialogOpen(true)}>
|
||||
{t("event.cleanup")}
|
||||
</SecondaryButton>
|
||||
|
||||
{selected.length > 0 && !isMobile && (
|
||||
<>
|
||||
<Divider orientation="vertical" flexItem />
|
||||
|
|
|
|||
|
|
@ -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<unknown>) => 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 (
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5}>
|
||||
{values.map((val) => (
|
||||
<SquareChip key={val} label={getTaskStatusText(val as TaskStatus, t)} size="small" />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DenseSelect
|
||||
fullWidth={fullWidth}
|
||||
multiple
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
renderValue={renderValue || defaultRenderValue}
|
||||
displayEmpty={displayEmpty}
|
||||
>
|
||||
{showAllOption && (
|
||||
<SquareMenuItem value={[]} disabled>
|
||||
<ListItemText
|
||||
primary={allOptionText || t("task.allTaskStatuses")}
|
||||
slotProps={{ primary: { variant: "body2", style: { fontStyle: "italic" } } }}
|
||||
/>
|
||||
</SquareMenuItem>
|
||||
)}
|
||||
{Object.values(TaskStatus).map((status) => (
|
||||
<SquareMenuItem value={status} key={status}>
|
||||
<ListItemText primary={getTaskStatusText(status, t)} slotProps={{ primary: { variant: "body2" } }} />
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
</DenseSelect>
|
||||
{helperText && <FormHelperText>{helperText}</FormHelperText>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskStatusSelector;
|
||||
|
|
@ -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<unknown>) => 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 (
|
||||
<Box display="flex" flexWrap="wrap" gap={0.5}>
|
||||
{values.map((val) => (
|
||||
<SquareChip key={val} label={t(`task.${val}`)} size="small" />
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DenseSelect
|
||||
fullWidth={fullWidth}
|
||||
multiple
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
renderValue={renderValue || defaultRenderValue}
|
||||
displayEmpty={displayEmpty}
|
||||
>
|
||||
{showAllOption && (
|
||||
<SquareMenuItem value={[]} disabled>
|
||||
<ListItemText
|
||||
primary={allOptionText || t("task.allTaskTypes")}
|
||||
slotProps={{ primary: { variant: "body2", style: { fontStyle: "italic" } } }}
|
||||
/>
|
||||
</SquareMenuItem>
|
||||
)}
|
||||
{Object.values(TaskType).map((type) => (
|
||||
<SquareMenuItem value={type} key={type}>
|
||||
<ListItemText primary={t(`task.${type}`)} slotProps={{ primary: { variant: "body2" } }} />
|
||||
</SquareMenuItem>
|
||||
))}
|
||||
</DenseSelect>
|
||||
{helperText && <FormHelperText>{helperText}</FormHelperText>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaskTypeSelector;
|
||||
Loading…
Reference in New Issue