mirror of
https://github.com/certimate-go/certimate.git
synced 2026-10-25 10:14:24 +08:00
feat(ui): home ui
This commit is contained in:
@@ -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;
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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": "前往工作流"
|
||||
}
|
||||
|
||||
@@ -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} />;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user