feat(ui): home ui

This commit is contained in:
Fu Diwei
2025-08-13 11:02:22 +08:00
parent c1f48bdc30
commit 947a2653c1
9 changed files with 196 additions and 206 deletions
@@ -17,7 +17,7 @@ import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/wor
import { mergeCls } from "@/utils/css";
import { getErrMsg } from "@/utils/error";
import WorkflowStatusIcon from "./WorkflowStatusIcon";
import WorkflowStatus from "./WorkflowStatus";
export interface WorkflowRunDetailProps {
className?: string;
@@ -88,34 +88,6 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
const [showTimestamp, setShowTimestamp] = useState(true);
const [showWhitespace, setShowWhitespace] = useState(true);
const renderBadge = () => {
let color: string | undefined;
switch (runStatus) {
case WORKFLOW_RUN_STATUSES.PENDING:
break;
case WORKFLOW_RUN_STATUSES.RUNNING:
color = themeToken.colorInfo;
break;
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
color = themeToken.colorSuccess;
break;
case WORKFLOW_RUN_STATUSES.FAILED:
color = themeToken.colorError;
break;
case WORKFLOW_RUN_STATUSES.CANCELED:
color = themeToken.colorWarning;
break;
}
return (
<Flex gap="small" style={{ color: color }}>
<WorkflowStatusIcon size="1.25em" status={runStatus} />
{t(`workflow_run.props.status.${runStatus}`)}
</Flex>
);
};
const renderRecord = (record: Log) => {
let message = <>{record.message}</>;
if (record.data != null && Object.keys(record.data).length > 0) {
@@ -189,7 +161,9 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
<Typography.Title level={5}>{t("workflow_run.logs")}</Typography.Title>
<div className="rounded-md bg-black text-stone-200">
<div className="flex items-center gap-2 p-4">
<div className="grow overflow-hidden">{renderBadge()}</div>
<div className="grow overflow-hidden">
<WorkflowStatus value={runStatus} />
</div>
<div>
<Dropdown
menu={{
@@ -0,0 +1,153 @@
import { useTranslation } from "react-i18next";
import {
IconCircleCheck,
IconCircleCheckFilled,
IconCircleDashed,
IconCircleOff,
IconCircleX,
IconCircleXFilled,
IconClock,
IconClockFilled,
IconLoader3,
} from "@tabler/icons-react";
import { Typography, theme } from "antd";
import { WORKFLOW_RUN_STATUSES, type WorkflorRunStatusType } from "@/domain/workflowRun";
import { mergeCls } from "@/utils/css";
const useColor = (value: WorkflorRunStatusType | string, defaultColor?: string | false) => {
const { token: themeToken } = theme.useToken();
switch (value) {
case WORKFLOW_RUN_STATUSES.PENDING:
if (defaultColor == null || !defaultColor) {
return themeToken.colorTextSecondary;
}
break;
case WORKFLOW_RUN_STATUSES.RUNNING:
if (defaultColor == null || !defaultColor) {
return themeToken.colorInfo;
}
break;
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
if (defaultColor == null || !defaultColor) {
return themeToken.colorSuccess;
}
break;
case WORKFLOW_RUN_STATUSES.FAILED:
if (defaultColor == null || !defaultColor) {
return themeToken.colorError;
}
break;
case WORKFLOW_RUN_STATUSES.CANCELED:
if (defaultColor == null || !defaultColor) {
return themeToken.colorWarning;
}
break;
default:
if (defaultColor == null || !defaultColor) {
return themeToken.colorTextSecondary;
}
break;
}
return defaultColor;
};
export interface WorkflowStatusIconProps {
className?: string;
style?: React.CSSProperties;
color?: string | false;
size?: number | string;
type?: "filled" | "outlined";
value: WorkflorRunStatusType | string;
}
const WorkflowStatusIcon = ({ className, style, size = "1.25em", type = "outlined", value, ...props }: WorkflowStatusIconProps) => {
const color = useColor(value, props.color);
switch (value) {
case WORKFLOW_RUN_STATUSES.PENDING:
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
{type === "filled" ? <IconClockFilled color={color} size={size} /> : <IconClock color={color} size={size} />}
</span>
);
case WORKFLOW_RUN_STATUSES.RUNNING:
return (
<span className={mergeCls("anticon", "animate-spin", className)} style={style} role="img">
<IconLoader3 color={color} size={size} />
</span>
);
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
{type === "filled" ? <IconCircleCheckFilled color={color} size={size} /> : <IconCircleCheck color={color} size={size} />}
</span>
);
case WORKFLOW_RUN_STATUSES.FAILED:
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
{type === "filled" ? <IconCircleXFilled color={color} size={size} /> : <IconCircleX color={color} size={size} />}
</span>
);
case WORKFLOW_RUN_STATUSES.CANCELED:
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
<IconCircleOff color={color} size={size} />
</span>
);
default:
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
<IconCircleDashed color={color} size={size} />
</span>
);
}
};
export interface WorkflowStatusProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
color?: string | false;
showIcon?: boolean;
type?: WorkflowStatusIconProps["type"];
value: WorkflorRunStatusType | string;
}
const WorkflowStatus = ({ className, style, children, showIcon = true, type, value, ...props }: WorkflowStatusProps) => {
const { t } = useTranslation();
const color = useColor(value, props.color);
const renderIcon = () => (showIcon ? <WorkflowStatusIcon type={type} value={value} /> : null);
switch (value) {
case WORKFLOW_RUN_STATUSES.PENDING:
case WORKFLOW_RUN_STATUSES.RUNNING:
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
case WORKFLOW_RUN_STATUSES.FAILED:
case WORKFLOW_RUN_STATUSES.CANCELED:
return (
<Typography.Text className={className} style={style}>
<div className="flex items-center gap-2">
{renderIcon()}
{children != null ? children : <span style={{ color: color }}>{t(`workflow_run.props.status.${value.toLowerCase()}`)}</span>}
</div>
</Typography.Text>
);
default:
return (
<Typography.Text className={className} style={style}>
<div className="flex items-center gap-2">{children != null ? children : <></>}</div>
</Typography.Text>
);
}
};
const _default = Object.assign(WorkflowStatus, {
Icon: WorkflowStatusIcon,
});
export default _default;
@@ -1,78 +0,0 @@
import { IconCircleCheck, IconCircleDashed, IconCircleOff, IconCircleX, IconClock, IconLoader3 } from "@tabler/icons-react";
import { theme } from "antd";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { mergeCls } from "@/utils/css";
export interface WorkflowStatusIconProps {
className?: string;
style?: React.CSSProperties;
color?: string | true;
size?: number | string;
status: string;
}
const WorkflowStatusIcon = ({ className, style, color, status, ...props }: WorkflowStatusIconProps) => {
const { size = "1em" } = props;
const { token: themeToken } = theme.useToken();
switch (status) {
case WORKFLOW_RUN_STATUSES.PENDING:
if (color === true) {
color = themeToken.colorTextSecondary;
}
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
<IconClock color={color} size={size} />
</span>
);
case WORKFLOW_RUN_STATUSES.RUNNING:
if (color === true) {
color = themeToken.colorInfo;
}
return (
<span className={mergeCls("anticon", "animate-spin", className)} style={style} role="img">
<IconLoader3 color={color} size={size} />
</span>
);
case WORKFLOW_RUN_STATUSES.SUCCEEDED:
if (color === true) {
color = themeToken.colorSuccess;
}
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
<IconCircleCheck color={color} size={size} />
</span>
);
case WORKFLOW_RUN_STATUSES.FAILED:
if (color === true) {
color = themeToken.colorError;
}
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
<IconCircleX color={color} size={size} />
</span>
);
case WORKFLOW_RUN_STATUSES.CANCELED:
if (color === true) {
color = themeToken.colorWarning;
}
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
<IconCircleOff color={color} size={size} />
</span>
);
default:
if (color === true) {
color = themeToken.colorTextSecondary;
}
return (
<span className={mergeCls("anticon", className)} style={style} role="img">
<IconCircleDashed color={color} size={size} />
</span>
);
}
};
export default WorkflowStatusIcon;
@@ -1,55 +0,0 @@
import { useTranslation } from "react-i18next";
import { Tag } from "antd";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import WorkflowStatusIcon from "./WorkflowStatusIcon";
export interface 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-warning)" icon={Icon}>
{t("workflow_run.props.status.canceled")}
</Tag>
);
default:
return <></>;
}
};
export default WorkflowStatusTag;
+6 -6
View File
@@ -7,12 +7,12 @@
"dashboard.statistics.all_workflows": "All workflows",
"dashboard.statistics.enabled_workflows": "Active workflows",
"dashboard.shortcut": "Shortcuts",
"dashboard.shortcut.create_workflow": "Create new workflow",
"dashboard.shortcut.change_account": "Change username or password",
"dashboard.shortcut.configure_ca": "Configure certificate authorities",
"dashboard.latest_workflow_runs": "Latest workflow runs",
"dashboard.latest_workflow_runs.nodata.description": "It looks like you don't have any runs. Get started by running a workflow.",
"dashboard.latest_workflow_runs.nodata.button": "Go to workflows",
"dashboard.quick_actions": "Quick actions",
"dashboard.quick_actions.create_workflow": "Create workflow",
"dashboard.quick_actions.change_password": "Change password",
"dashboard.quick_actions.configure_ca": "Configure CA"
"dashboard.latest_workflow_runs.nodata.button": "Go to workflows"
}
+6 -6
View File
@@ -7,12 +7,12 @@
"dashboard.statistics.all_workflows": "所有工作流",
"dashboard.statistics.enabled_workflows": "已启用工作流",
"dashboard.shortcut": "快捷操作",
"dashboard.shortcut.create_workflow": "创建新的工作流",
"dashboard.shortcut.change_account": "修改登录账号密码",
"dashboard.shortcut.configure_ca": "配置证书颁发机构",
"dashboard.latest_workflow_runs": "最近运行的工作流",
"dashboard.latest_workflow_runs.nodata.description": "当前未找到任何工作流运行历史。请先运行一个工作流。",
"dashboard.latest_workflow_runs.nodata.button": "前往工作流",
"dashboard.quick_actions": "快捷操作",
"dashboard.quick_actions.create_workflow": "新建工作流",
"dashboard.quick_actions.change_password": "修改登录密码",
"dashboard.quick_actions.configure_ca": "配置证书颁发机构"
"dashboard.latest_workflow_runs.nodata.button": "前往工作流"
}
+21 -24
View File
@@ -15,14 +15,14 @@ import {
IconShieldCheckered,
} from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { App, Button, Card, Col, Divider, Flex, Grid, Row, Skeleton, Table, type TableProps, Typography } from "antd";
import { App, Button, Card, Col, Row, Skeleton, Table, type TableProps, Typography } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import { get as getStatistics } from "@/api/statistics";
import Empty from "@/components/Empty";
import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer";
import WorkflowStatusTag from "@/components/workflow/WorkflowStatusTag";
import WorkflowStatus from "@/components/workflow/WorkflowStatus";
import { type Statistics } from "@/domain/statistics";
import { type WorkflowRunModel } from "@/domain/workflowRun";
import { useBrowserTheme } from "@/hooks";
@@ -35,8 +35,6 @@ const Dashboard = () => {
const { t } = useTranslation();
const breakpoints = Grid.useBreakpoint();
return (
<div className="px-6 py-4">
<div className="container">
@@ -48,26 +46,25 @@ const Dashboard = () => {
<StatisticCards />
</div>
<Divider />
<div className="mt-8">
<h3>{t("dashboard.shortcut")}</h3>
<div className="flex items-center gap-4 not-md:flex-wrap">
<Button icon={<IconCirclePlus size="1em" />} shape="round" size="large" type="primary" onClick={() => navigate("/workflows/new")}>
<span className="text-sm">{t("dashboard.shortcut.create_workflow")}</span>
</Button>
<Button icon={<IconLock size="1em" />} shape="round" size="large" onClick={() => navigate("/settings/account")}>
<span className="text-sm">{t("dashboard.shortcut.change_account")}</span>
</Button>
<Button icon={<IconPlugConnected size="1em" />} shape="round" size="large" onClick={() => navigate("/settings/ssl-provider")}>
<span className="text-sm">{t("dashboard.shortcut.configure_ca")}</span>
</Button>
</div>
</div>
<Flex justify="stretch" vertical={!breakpoints.lg} gap={16}>
<Card className="transition-all max-lg:flex-1 lg:w-[280px] xl:w-[360px]" title={t("dashboard.quick_actions")}>
<div className="flex flex-col gap-4">
<Button block type="primary" size="large" icon={<IconCirclePlus size="1.25em" />} onClick={() => navigate("/workflows/new")}>
<span className="text-sm">{t("dashboard.quick_actions.create_workflow")}</span>
</Button>
<Button block size="large" icon={<IconLock size="1.25em" />} onClick={() => navigate("/settings/account")}>
<span className="text-sm">{t("dashboard.quick_actions.change_password")}</span>
</Button>
<Button block size="large" icon={<IconPlugConnected size="1.25em" />} onClick={() => navigate("/settings/ssl-provider")}>
<span className="text-sm">{t("dashboard.quick_actions.configure_ca")}</span>
</Button>
</div>
</Card>
<Card className="flex-1" title={t("dashboard.latest_workflow_runs")}>
<WorkflowRunHistoryTable />
</Card>
</Flex>
<div className="mt-8">
<h3>{t("dashboard.latest_workflow_runs")}</h3>
<WorkflowRunHistoryTable />
</div>
</div>
</div>
);
@@ -268,7 +265,7 @@ const WorkflowRunHistoryTable = ({ className, style }: { className?: string; sty
key: "status",
title: t("workflow_run.props.status"),
render: (_, record) => {
return <WorkflowStatusTag status={record.status} />;
return <WorkflowStatus type="filled" value={record.status} />;
},
},
{
@@ -11,7 +11,7 @@ import Empty from "@/components/Empty";
import Show from "@/components/Show";
import Tips from "@/components/Tips";
import WorkflowRunDetailDrawer from "@/components/workflow/WorkflowRunDetailDrawer";
import WorkflowStatusTag from "@/components/workflow/WorkflowStatusTag";
import WorkflowStatus from "@/components/workflow/WorkflowStatus";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { useAppSettings, useZustandShallowSelector } from "@/hooks";
@@ -52,7 +52,7 @@ const WorkflowDetailRuns = () => {
key: "status",
title: t("workflow_run.props.status"),
render: (_, record) => {
return <WorkflowStatusTag status={record.status} />;
return <WorkflowStatus type="filled" value={record.status} />;
},
},
{
+4 -5
View File
@@ -10,7 +10,7 @@ import { ClientResponseError } from "pocketbase";
import { startRun as startWorkflowRun } from "@/api/workflows";
import Empty from "@/components/Empty";
import Show from "@/components/Show";
import WorkflowStatusIcon from "@/components/workflow/WorkflowStatusIcon";
import WorkflowStatus from "@/components/workflow/WorkflowStatus";
import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow";
import { useAppSettings } from "@/hooks";
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
@@ -108,10 +108,9 @@ const WorkflowList = () => {
return <></>;
} else {
return (
<Flex gap="small">
<WorkflowStatusIcon color={true} size="1.25em" status={lastRunStatus} />
<Typography.Text>{lastRunTime ? dayjs(lastRunTime).format("YYYY-MM-DD HH:mm:ss") : ""}</Typography.Text>
</Flex>
<WorkflowStatus type="filled" value={lastRunStatus}>
{lastRunTime ? dayjs(lastRunTime).format("YYYY-MM-DD HH:mm:ss") : ""}
</WorkflowStatus>
);
}
},