feat(ui): new WorkflowList page

This commit is contained in:
Fu Diwei
2025-07-20 17:07:51 +08:00
parent f88c321554
commit ec2a29f6c2
8 changed files with 185 additions and 138 deletions
+1 -1
View File
@@ -59,12 +59,12 @@ const RootApp = () => {
token: {
/* @see global.css, YOU MUST MODIFY BOTH DEFINITIONS AT THE SAME TIME! */
colorPrimary: browserTheme === "dark" ? "#f97316" : "#ea580c",
colorLink: browserTheme === "dark" ? "#f97316" : "#ea580c",
colorInfo: browserTheme === "dark" ? "#478be6" : "#0969da",
colorSuccess: browserTheme === "dark" ? "#57ab5a" : "#1a7f37",
colorWarning: browserTheme === "dark" ? "#daaa3f" : "#eac54f",
colorError: browserTheme === "dark" ? "#e5534b" : "#d1242f",
colorBgContainer: "var(--color-container)",
colorLink: "var(--color-primary)",
},
components: {
Layout: {
@@ -1,31 +1,8 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons";
import { IconBrowserShare, IconCheck, IconChevronRight, IconDownload, IconSettings2 } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import {
Button,
Collapse,
Divider,
Dropdown,
Empty,
Flex,
Skeleton,
Space,
Spin,
Table,
type TableProps,
Tooltip,
Typography,
notification,
theme,
} from "antd";
import { Button, Collapse, Divider, Dropdown, Empty, Flex, Skeleton, Spin, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
@@ -40,6 +17,8 @@ import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/wor
import { mergeCls } from "@/utils/css";
import { getErrMsg } from "@/utils/error";
import WorkflowStatusIcon from "./WorkflowStatusIcon";
export type WorkflowRunDetailProps = {
className?: string;
style?: React.CSSProperties;
@@ -114,35 +93,35 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
case WORKFLOW_RUN_STATUSES.PENDING:
return (
<Flex gap="small">
<ClockCircleOutlinedIcon />
<WorkflowStatusIcon status={runStatus} />
{t("workflow_run.props.status.pending")}
</Flex>
);
case WORKFLOW_RUN_STATUSES.RUNNING:
return (
<Flex gap="small" style={{ color: themeToken.colorInfo }}>
<SyncOutlinedIcon spin />
<WorkflowStatusIcon status={runStatus} />
{t("workflow_run.props.status.running")}
</Flex>
);
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
return (
<Flex gap="small" style={{ color: themeToken.colorSuccess }}>
<CheckCircleOutlinedIcon />
<WorkflowStatusIcon status={runStatus} />
{t("workflow_run.props.status.succeeded")}
</Flex>
);
case WORKFLOW_RUN_STATUSES.FAILED:
return (
<Flex gap="small" style={{ color: themeToken.colorError }}>
<CloseCircleOutlinedIcon />
<WorkflowStatusIcon status={runStatus} />
{t("workflow_run.props.status.failed")}
</Flex>
);
case WORKFLOW_RUN_STATUSES.CANCELED:
return (
<Flex gap="small" style={{ color: themeToken.colorWarning }}>
<StopOutlinedIcon />
<WorkflowStatusIcon status={runStatus} />
{t("workflow_run.props.status.canceled")}
</Flex>
);
@@ -329,7 +308,7 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
align: "end",
width: 120,
render: (_, record) => (
<Space.Compact>
<div className="flex items-center justify-end">
<CertificateDetailDrawer
data={record}
trigger={
@@ -338,7 +317,7 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
</Tooltip>
}
/>
</Space.Compact>
</div>
),
},
];
+7 -42
View File
@@ -1,20 +1,13 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
PauseOutlined as PauseOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons";
import { IconBrowserShare, IconTrash } from "@tabler/icons-react";
import { IconBrowserShare, IconPlayerPause, IconTrash } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { Alert, Button, Empty, Modal, Space, Table, type TableProps, Tag, Tooltip, notification } from "antd";
import { Alert, Button, Empty, Modal, Table, type TableProps, Tooltip, notification } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import { cancelRun as cancelWorkflowRun } from "@/api/workflows";
import WorkflowStatusTag from "@/components/workflow/WorkflowStatusTag";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import {
@@ -57,35 +50,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
title: t("workflow_run.props.status"),
ellipsis: true,
render: (_, record) => {
if (record.status === WORKFLOW_RUN_STATUSES.PENDING) {
return <Tag icon={<ClockCircleOutlinedIcon />}>{t("workflow_run.props.status.pending")}</Tag>;
} else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) {
return (
<Tag icon={<SyncOutlinedIcon spin />} color="processing">
{t("workflow_run.props.status.running")}
</Tag>
);
} else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
return (
<Tag icon={<CheckCircleOutlinedIcon />} color="success">
{t("workflow_run.props.status.succeeded")}
</Tag>
);
} else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) {
return (
<Tag icon={<CloseCircleOutlinedIcon />} color="error">
{t("workflow_run.props.status.failed")}
</Tag>
);
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return (
<Tag icon={<StopOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")}
</Tag>
);
}
return <></>;
return <WorkflowStatusTag status={record.status} />;
},
},
{
@@ -139,7 +104,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
record.status === WORKFLOW_RUN_STATUSES.CANCELED;
return (
<Space.Compact>
<div className="flex items-center justify-end">
<WorkflowRunDetailDrawer
data={record}
trigger={
@@ -153,7 +118,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
<Button
color="default"
disabled={!allowCancel}
icon={<PauseOutlinedIcon />}
icon={<IconPlayerPause size="1.25em" />}
variant="text"
onClick={() => {
handleCancelClick(record);
@@ -173,7 +138,7 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
}}
/>
</Tooltip>
</Space.Compact>
</div>
);
},
},
@@ -0,0 +1,101 @@
import {
CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons";
import { theme } from "antd";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { mergeCls } from "@/utils/css";
export type WorkflowStatusIconProps = {
className?: string;
style?: React.CSSProperties;
color?: string | true;
size?: number | string;
status: string;
};
const WorkflowStatusIcon = ({ className, style, color, size, status }: WorkflowStatusIconProps) => {
const { token: themeToken } = theme.useToken();
switch (status) {
case WORKFLOW_RUN_STATUSES.PENDING:
if (color === true) {
color = themeToken.colorTextSecondary;
}
return (
<ClockCircleOutlinedIcon
className={className}
style={{
...style,
color: color,
fontSize: size,
}}
/>
);
case WORKFLOW_RUN_STATUSES.RUNNING:
if (color === true) {
color = themeToken.colorInfo;
}
return (
<SyncOutlinedIcon
className={mergeCls("animate-spin", className)}
style={{
...style,
color: color,
fontSize: size,
}}
/>
);
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
if (color === true) {
color = themeToken.colorSuccess;
console.log(color);
}
return (
<CheckCircleOutlinedIcon
className={className}
style={{
...style,
color: color,
fontSize: size,
}}
/>
);
case WORKFLOW_RUN_STATUSES.FAILED:
if (color === true) {
color = themeToken.colorError;
}
return (
<CloseCircleOutlinedIcon
className={className}
style={{
...style,
color: color,
fontSize: size,
}}
/>
);
case WORKFLOW_RUN_STATUSES.CANCELED:
if (color === true) {
color = themeToken.colorWarning;
}
return (
<StopOutlinedIcon
className={className}
style={{
...style,
color: color,
fontSize: size,
}}
/>
);
default:
return <></>;
}
};
export default WorkflowStatusIcon;
@@ -0,0 +1,55 @@
import { useTranslation } from "react-i18next";
import { Tag } from "antd";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import WorkflowStatusIcon from "./WorkflowStatusIcon";
export type WorkflowStatusTagProps = {
className?: string;
style?: React.CSSProperties;
status: string;
};
const WorkflowStatusTag = ({ className, style, status }: WorkflowStatusTagProps) => {
const { t } = useTranslation();
const Icon = <WorkflowStatusIcon status={status} />;
switch (status) {
case WORKFLOW_RUN_STATUSES.PENDING:
return (
<Tag className={className} style={style} icon={Icon}>
{t("workflow_run.props.status.pending")}
</Tag>
);
case WORKFLOW_RUN_STATUSES.RUNNING:
return (
<Tag className={className} style={style} color="var(--color-info)" icon={Icon}>
{t("workflow_run.props.status.running")}
</Tag>
);
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
return (
<Tag className={className} style={style} color="var(--color-success)" icon={Icon}>
{t("workflow_run.props.status.succeeded")}
</Tag>
);
case WORKFLOW_RUN_STATUSES.FAILED:
return (
<Tag className={className} style={style} color="var(--color-error)" icon={Icon}>
{t("workflow_run.props.status.failed")}
</Tag>
);
case WORKFLOW_RUN_STATUSES.CANCELED:
return (
<Tag className={className} style={style} color="var(--color-danger)" icon={Icon}>
{t("workflow_run.props.status.canceled")}
</Tag>
);
default:
return <></>;
}
};
export default WorkflowStatusTag;
+6 -40
View File
@@ -1,13 +1,6 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import {
CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons";
import {
IconActivity,
IconBrowserShare,
@@ -20,14 +13,15 @@ import {
IconUserShield,
} from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { App, Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, type TableProps, Tag, Typography, theme } from "antd";
import { App, Button, Card, Col, Divider, Empty, Flex, Grid, Row, Space, Statistic, Table, type TableProps, Typography, theme } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import { get as getStatistics } from "@/api/statistics";
import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer";
import WorkflowStatusTag from "@/components/workflow/WorkflowStatusTag";
import { type Statistics } from "@/domain/statistics";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { type WorkflowRunModel } from "@/domain/workflowRun";
import { list as listWorkflowRuns } from "@/repository/workflowRun";
import { getErrMsg } from "@/utils/error";
@@ -102,35 +96,7 @@ const Dashboard = () => {
title: t("workflow_run.props.status"),
ellipsis: true,
render: (_, record) => {
if (record.status === WORKFLOW_RUN_STATUSES.PENDING) {
return <Tag icon={<ClockCircleOutlinedIcon />}>{t("workflow_run.props.status.pending")}</Tag>;
} else if (record.status === WORKFLOW_RUN_STATUSES.RUNNING) {
return (
<Tag icon={<SyncOutlinedIcon spin />} color="processing">
{t("workflow_run.props.status.running")}
</Tag>
);
} else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
return (
<Tag icon={<CheckCircleOutlinedIcon />} color="success">
{t("workflow_run.props.status.succeeded")}
</Tag>
);
} else if (record.status === WORKFLOW_RUN_STATUSES.FAILED) {
return (
<Tag icon={<CloseCircleOutlinedIcon />} color="error">
{t("workflow_run.props.status.failed")}
</Tag>
);
} else if (record.status === WORKFLOW_RUN_STATUSES.CANCELED) {
return (
<Tag icon={<StopOutlinedIcon />} color="warning">
{t("workflow_run.props.status.canceled")}
</Tag>
);
}
return <></>;
return <WorkflowStatusTag status={record.status} />;
},
},
{
@@ -162,9 +128,9 @@ const Dashboard = () => {
align: "end",
width: 120,
render: (_, record) => (
<Space.Compact>
<div className="flex items-center justify-end">
<WorkflowRunDetailDrawer data={record} trigger={<Button color="primary" icon={<IconBrowserShare size="1.25em" />} variant="text" />} />
</Space.Compact>
</div>
),
},
];
+4 -23
View File
@@ -1,19 +1,13 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import {
CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
StopOutlined as StopOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons";
import { IconCopy, IconEdit, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { App, Button, Divider, Empty, Input, Menu, type MenuProps, Radio, Space, Switch, Table, type TableProps, Tooltip, Typography, theme } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import WorkflowStatusIcon from "@/components/workflow/WorkflowStatusIcon";
import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
@@ -136,22 +130,9 @@ const WorkflowList = () => {
key: "lastRun",
title: t("workflow.props.last_run_at"),
render: (_, record) => {
let icon = <></>;
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.PENDING) {
icon = <ClockCircleOutlinedIcon style={{ color: themeToken.colorTextSecondary }} />;
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.RUNNING) {
icon = <SyncOutlinedIcon style={{ color: themeToken.colorInfo }} spin />;
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
icon = <CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />;
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
icon = <CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />;
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.CANCELED) {
icon = <StopOutlinedIcon style={{ color: themeToken.colorWarning }} />;
}
return (
<Space>
{icon}
<WorkflowStatusIcon color={true} status={record.lastRunStatus!} />
<Typography.Text>{record.lastRunTime ? dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
</Space>
);
@@ -179,7 +160,7 @@ const WorkflowList = () => {
fixed: "right",
width: 120,
render: (_, record) => (
<Space.Compact>
<div className="flex items-center justify-end">
<Tooltip title={t("workflow.action.edit")}>
<Button
color="primary"
@@ -213,7 +194,7 @@ const WorkflowList = () => {
}}
/>
</Tooltip>
</Space.Compact>
</div>
),
},
];
+1 -1
View File
@@ -117,7 +117,7 @@ const WorkflowNew = () => {
<div className="px-6 py-4">
<div className="mx-auto max-w-320">
<h1>{t("workflow.new.title")}</h1>
<p className="text-base text-gray-500">{t("workflow.new.subtitle")}</p>
<p className="mb-0 text-base text-gray-500">{t("workflow.new.subtitle")}</p>
</div>
</div>
</Card>