feat(dashboard): admin can now view task detailed steps

This commit is contained in:
Aaron Liu 2025-05-18 11:31:10 +08:00
parent 756c4518c7
commit ec341164d5
7 changed files with 97 additions and 104 deletions

View File

@ -510,6 +510,7 @@ export interface Task extends CommonMixin {
};
user_hash_id?: string;
task_hash_id?: string;
summary?: TaskSummary;
node?: Node;
}

View File

@ -6,7 +6,7 @@ import { TaskSummary, TaskType } from "../../../api/workflow";
import CrUri, { Filesystem } from "../../../util/uri";
import TaskSummaryTitle from "../../Pages/Tasks/TaskSummaryTitle";
const userTaskTypes: string[] = [
export const userTaskTypes: string[] = [
TaskType.relocate,
TaskType.create_archive,
TaskType.extract_archive,

View File

@ -29,9 +29,10 @@ import UserAvatar from "../../../Common/User/UserAvatar";
import FileBadge from "../../../FileManager/FileBadge";
import SettingForm from "../../../Pages/Setting/SettingForm";
import DownloadFileList from "../../../Pages/Tasks/DownloadFileList";
import TaskProgress from "../../../Pages/Tasks/TaskProgress";
import { getTaskStatusText } from "../../../Pages/Tasks/TaskProps";
import UserDialog from "../../User/UserDialog/UserDialog";
import { processTaskContent } from "../TaskContent";
import { processTaskContent, userTaskTypes } from "../TaskContent";
import BlobErrors from "./BlobErrors";
dayjs.extend(duration);
@ -66,6 +67,10 @@ const TaskForm = ({ values }: { values: Task }) => {
return res;
}, [values]);
const isUserTask = useMemo(() => {
return userTaskTypes.includes(values.type ?? "");
}, [values]);
return (
<>
<UserDialog open={userDialogOpen} onClose={() => setUserDialogOpen(false)} userID={userDialogID} />
@ -225,6 +230,18 @@ const TaskForm = ({ values }: { values: Task }) => {
</SettingForm>
)}
{isUserTask && (
<SettingForm title={t("application:setting.taskProgress")} noContainer lgWidth={12}>
<TaskProgress
taskId={values.task_hash_id ?? ""}
taskStatus={values.status?.toString() ?? ""}
taskType={values.type ?? ""}
summary={values.summary}
node={values.node ? { type: values.node.type?.toString() ?? "" } : undefined}
/>
</SettingForm>
)}
{values?.public_state?.error_history && (
<SettingForm title={t("application:setting.retryErrorHistory")} noContainer lgWidth={12}>
<TableContainer component={StyledTableContainerPaper}>

View File

@ -3,13 +3,13 @@ import HoverPopover from "material-ui-popup-state/HoverPopover";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { getTasksPhaseProgress } from "../../../api/api.ts";
import { TaskProgress, TaskProgresses, TaskResponse } from "../../../api/workflow.ts";
import { TaskProgress, TaskProgresses } from "../../../api/workflow.ts";
import { useAppDispatch } from "../../../redux/hooks.ts";
import { sizeToString } from "../../../util";
import StepProgressBar from "./StepProgressBar.tsx";
export interface StepProgressPopoverProps extends PopoverProps {
task: TaskResponse;
taskId: string;
}
export const ProgressKeys = {
@ -167,7 +167,7 @@ const ProgressBar = ({ pkey, p }: { pkey: string; p: TaskProgress }) => {
}
};
const StepProgressPopover = ({ task, open, ...rest }: StepProgressPopoverProps) => {
const StepProgressPopover = ({ taskId, open, ...rest }: StepProgressPopoverProps) => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const stopPropagation = useCallback((e: any) => e.stopPropagation(), []);
@ -175,9 +175,9 @@ const StepProgressPopover = ({ task, open, ...rest }: StepProgressPopoverProps)
useEffect(() => {
if (open) {
dispatch(getTasksPhaseProgress(task.id)).then((res) => setProgress(res));
dispatch(getTasksPhaseProgress(taskId)).then((res) => setProgress(res));
}
}, [open, task]);
}, [open, taskId]);
return (
<HoverPopover

View File

@ -47,7 +47,13 @@ const TaskDetail = ({ task, downloading }: TaskDetailProps) => {
</Alert>
)}
{task.status == TaskStatus.error && <Alert severity={"error"}>{task.error}</Alert>}
<TaskProgress task={task} />
<TaskProgress
taskId={task.id}
taskStatus={task.status}
taskType={task.type}
summary={task.summary}
node={task.node}
/>
<Divider />
</Stack>
<Stack spacing={1}>

View File

@ -1,12 +1,18 @@
import { Box, Stepper, useMediaQuery, useTheme } from "@mui/material";
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { NodeTypes, TaskResponse, TaskStatus, TaskType } from "../../../api/workflow.ts";
import { NodeTypes, TaskStatus, TaskSummary, TaskType } from "../../../api/workflow.ts";
import PieceProgress from "./PieceProgress.tsx";
import TaskProgressStep from "./TaskProgressStep.tsx";
export interface TaskProgressProps {
task: TaskResponse;
taskId: string;
taskStatus: string;
taskType: string;
summary?: TaskSummary;
node?: {
type: string;
};
}
interface StepModel {
@ -191,56 +197,52 @@ const stepOptions: {
],
};
const TaskProgress = ({ task }: TaskProgressProps) => {
const TaskProgress = ({ taskId, taskStatus, taskType, summary, node }: TaskProgressProps) => {
const { t } = useTranslation();
const [activeStep, setActiveStep] = useState(0);
const steps = useMemo((): StepModel[] => {
return stepOptions[task.type]?.[task.node?.type == NodeTypes.slave ? 1 : 0] ?? [];
}, [task.id, task.node?.type]);
return stepOptions[taskType]?.[node?.type == NodeTypes.slave ? 1 : 0] ?? [];
}, [taskId, node?.type]);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down("md"));
useEffect(() => {
if (task.status == TaskStatus.queued) {
if (taskStatus == TaskStatus.queued) {
setActiveStep(0);
return;
}
if (task.status == TaskStatus.completed) {
if (taskStatus == TaskStatus.completed) {
setActiveStep(steps.length);
return;
}
let active = 1;
for (let i = 1; i < steps.length; i++) {
if (steps[i].state == task.summary?.phase) {
if (steps[i].state == summary?.phase) {
active = i;
}
}
setActiveStep(active);
}, [steps, task]);
}, [steps, taskStatus, summary?.phase]);
return (
<Box sx={{ p: 2 }}>
<Stepper activeStep={activeStep} orientation={isMobile ? "vertical" : "horizontal"}>
{steps.map((step, index) => (
<TaskProgressStep
progressing={activeStep == index && task.status != TaskStatus.error}
task={task}
progressing={activeStep == index && taskStatus != TaskStatus.error}
taskId={taskId}
taskStatus={taskStatus}
description={step.description}
title={step.title}
showProgress={step.supportProgress}
key={task.id + "_" + step.title}
key={taskId + "_" + step.title}
/>
))}
</Stepper>
{task.type == TaskType.remote_download &&
task.summary?.props.download?.pieces &&
task.summary?.phase == "monitor" && (
<PieceProgress
total={task.summary?.props.download.num_pieces ?? 1}
pieces={task.summary.props.download?.pieces}
/>
)}
{taskType == TaskType.remote_download && summary?.props.download?.pieces && summary?.phase == "monitor" && (
<PieceProgress total={summary?.props.download.num_pieces ?? 1} pieces={summary?.props.download?.pieces} />
)}
</Box>
);
};

View File

@ -1,76 +1,51 @@
import { TaskResponse, TaskStatus } from "../../../api/workflow.ts";
import {
Step,
StepIcon,
StepIconProps,
StepLabel,
Typography,
} from "@mui/material";
import { Step, StepIcon, StepIconProps, StepLabel, Typography } from "@mui/material";
import { StepProps } from "@mui/material/Step/Step";
import { useTranslation } from "react-i18next";
import FacebookCircularProgress from "../../Common/CircularProgress.tsx";
import DismissCircleFilled from "../../Icons/DismissCircleFilled.tsx";
import { useMemo } from "react";
import CheckCircleFilled from "../../Icons/CheckCircleFilled.tsx";
import { usePopupState } from "material-ui-popup-state/hooks";
import { bindHover, bindPopover } from "material-ui-popup-state";
import { usePopupState } from "material-ui-popup-state/hooks";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { TaskStatus } from "../../../api/workflow.ts";
import FacebookCircularProgress from "../../Common/CircularProgress.tsx";
import CheckCircleFilled from "../../Icons/CheckCircleFilled.tsx";
import DismissCircleFilled from "../../Icons/DismissCircleFilled.tsx";
import StepProgressPopover from "./StepProgressPopover.tsx";
interface ProgressStepIconProps extends StepIconProps {}
const ProgressStepIcon =
(task: TaskResponse) => (props: ProgressStepIconProps) => {
const { active, completed, icon, ...rest } = props;
const ProgressStepIcon = (status: string) => (props: ProgressStepIconProps) => {
const { active, completed, icon, ...rest } = props;
let newIcon = icon;
if (active) {
newIcon = <FacebookCircularProgress sx={{ pt: "5px" }} size={24} />;
if (task.status == TaskStatus.error) {
newIcon = (
<DismissCircleFilled
sx={{ fontSize: 28.5, color: (theme) => theme.palette.error.main }}
/>
);
}
} else if (completed) {
newIcon = (
<CheckCircleFilled
sx={{ fontSize: 28.5, color: (theme) => theme.palette.primary.main }}
/>
);
let newIcon = icon;
if (active) {
newIcon = <FacebookCircularProgress sx={{ pt: "5px" }} size={24} />;
if (status == TaskStatus.error) {
newIcon = <DismissCircleFilled sx={{ fontSize: 28.5, color: (theme) => theme.palette.error.main }} />;
}
} else if (completed) {
newIcon = <CheckCircleFilled sx={{ fontSize: 28.5, color: (theme) => theme.palette.primary.main }} />;
}
if (active && task.status == TaskStatus.error) {
newIcon = (
<DismissCircleFilled
sx={{ fontSize: 28.5, color: (theme) => theme.palette.error.main }}
/>
);
}
if (active && status == TaskStatus.error) {
newIcon = <DismissCircleFilled sx={{ fontSize: 28.5, color: (theme) => theme.palette.error.main }} />;
}
if (active && task.status == TaskStatus.canceled) {
newIcon = (
<DismissCircleFilled
sx={{
fontSize: 28.5,
color: (theme) => theme.palette.action.active,
}}
/>
);
}
return (
<StepIcon
icon={newIcon}
active={active}
completed={completed}
{...rest}
if (active && status == TaskStatus.canceled) {
newIcon = (
<DismissCircleFilled
sx={{
fontSize: 28.5,
color: (theme) => theme.palette.action.active,
}}
/>
);
};
}
return <StepIcon icon={newIcon} active={active} completed={completed} {...rest} />;
};
export interface TaskProgressStepProps extends StepProps {
task: TaskResponse;
taskId: string;
taskStatus: string;
title: string;
description?: string;
showProgress?: boolean;
@ -78,7 +53,8 @@ export interface TaskProgressStepProps extends StepProps {
}
const TaskProgressStep = ({
task,
taskId,
taskStatus,
title,
description,
showProgress,
@ -87,36 +63,27 @@ const TaskProgressStep = ({
}: TaskProgressStepProps) => {
const popupState = usePopupState({
variant: "popover",
popupId: `progress_${task.id}_${title}`,
popupId: `progress_${taskId}_${title}`,
});
const { open, ...restPopup } = bindPopover(popupState);
const StepIconComponent = useMemo(() => {
return ProgressStepIcon(task);
}, [task.status]);
return ProgressStepIcon(taskStatus);
}, [taskStatus]);
const { t } = useTranslation();
return (
<>
<Step
{...rest}
{...(showProgress && progressing ? bindHover(popupState) : [])}
>
<Step {...rest} {...(showProgress && progressing ? bindHover(popupState) : [])}>
<StepLabel
optional={
description ? (
<Typography variant={"caption"}>{t(description)}</Typography>
) : undefined
}
optional={description ? <Typography variant={"caption"}>{t(description)}</Typography> : undefined}
slots={{
stepIcon: StepIconComponent
stepIcon: StepIconComponent,
}}
>
{t(title)}
</StepLabel>
</Step>
{showProgress && progressing && (
<StepProgressPopover task={task} open={open} {...restPopup} />
)}
{showProgress && progressing && <StepProgressPopover taskId={taskId} open={open} {...restPopup} />}
</>
);
};