feat: workflow run status & time

This commit is contained in:
Fu Diwei
2025-01-04 22:07:01 +08:00
parent b686579acc
commit 3b9a7fe805
29 changed files with 505 additions and 181 deletions

View File

@@ -1,5 +1,6 @@
import { ClientResponseError } from "pocketbase";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { getPocketBase } from "@/repository/pocketbase";
export const run = async (id: string) => {
@@ -11,7 +12,8 @@ export const run = async (id: string) => {
"Content-Type": "application/json",
},
body: {
id,
workflowId: id,
trigger: WORKFLOW_TRIGGERS.MANUAL,
},
});

View File

@@ -4,7 +4,7 @@ import { Alert, Drawer, Typography } from "antd";
import dayjs from "dayjs";
import Show from "@/components/Show";
import { type WorkflowRunModel } from "@/domain/workflowRun";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { useTriggerElement } from "@/hooks";
export type WorkflowRunDetailDrawerProps = {
@@ -32,11 +32,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
<Drawer destroyOnClose open={open} loading={loading} placement="right" title={`runlog-${data?.id}`} width={640} onClose={() => setOpen(false)}>
<Show when={!!data}>
<Show when={data!.succeeded}>
<Show when={data!.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
<Alert showIcon type="success" message={<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>} />
</Show>
<Show when={!!data!.error}>
<Show when={data!.status === WORKFLOW_RUN_STATUSES.FAILED}>
<Alert showIcon type="error" message={<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>} />
</Show>

View File

@@ -2,14 +2,18 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import {
CheckCircleOutlined as CheckCircleOutlinedIcon,
ClockCircleOutlined as ClockCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
SelectOutlined as SelectOutlinedIcon,
SyncOutlined as SyncOutlinedIcon,
} from "@ant-design/icons";
import { useRequest } from "ahooks";
import { Button, Empty, Space, Table, type TableProps, Typography, notification, theme } from "antd";
import { Button, Empty, Table, type TableProps, Tag, notification } from "antd";
import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import { type WorkflowRunModel } from "@/domain/workflowRun";
import { WORKFLOW_TRIGGERS } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun";
import { list as listWorkflowRuns } from "@/repository/workflowRun";
import { getErrMsg } from "@/utils/error";
import WorkflowRunDetailDrawer from "./WorkflowRunDetailDrawer";
@@ -23,8 +27,6 @@ export type WorkflowRunsProps = {
const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
const { t } = useTranslation();
const { token: themeToken } = theme.useToken();
const [notificationApi, NotificationContextHolder] = notification.useNotification();
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
@@ -46,45 +48,67 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
title: t("workflow_run.props.status"),
ellipsis: true,
render: (_, record) => {
if (record.succeeded) {
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 (
<Space>
<CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />
<Typography.Text type="success">{t("workflow_run.props.status.succeeded")}</Typography.Text>
</Space>
<Tag icon={<SyncOutlinedIcon spin />} color="processing">
{t("workflow_run.props.status.running")}
</Tag>
);
} else {
} else if (record.status === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
return (
<Space>
<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />
<Typography.Text type="danger">{t("workflow_run.props.status.failed")}</Typography.Text>
</Space>
<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>
);
}
return <></>;
},
},
{
key: "trigger",
title: t("workflow_run.props.trigger"),
ellipsis: true,
render: () => {
return "TODO";
render: (_, record) => {
if (record.trigger === WORKFLOW_TRIGGERS.AUTO) {
return t("workflow_run.props.trigger.auto");
} else if (record.trigger === WORKFLOW_TRIGGERS.MANUAL) {
return t("workflow_run.props.trigger.manual");
}
return <></>;
},
},
{
key: "startedAt",
title: t("workflow_run.props.started_at"),
ellipsis: true,
render: () => {
return "TODO";
render: (_, record) => {
if (record.startedAt) {
return dayjs(record.startedAt).format("YYYY-MM-DD HH:mm:ss");
}
return <></>;
},
},
{
key: "completedAt",
title: t("workflow_run.props.completed_at"),
key: "endedAt",
title: t("workflow_run.props.ended_at"),
ellipsis: true,
render: () => {
return "TODO";
render: (_, record) => {
if (record.endedAt) {
return dayjs(record.endedAt).format("YYYY-MM-DD HH:mm:ss");
}
return <></>;
},
},
{

View File

@@ -13,6 +13,9 @@ export interface WorkflowModel extends BaseModel {
content?: WorkflowNode;
draft?: WorkflowNode;
hasDraft?: boolean;
lastRunId?: string;
lastRunStatus?: string;
lastRunTime?: string;
}
export const WORKFLOW_TRIGGERS = Object.freeze({

View File

@@ -1,8 +1,11 @@
export interface WorkflowRunModel extends BaseModel {
workflowId: string;
status: string;
trigger: string;
startedAt: ISO8601String;
endedAt: ISO8601String;
logs: WorkflowRunLog[];
error: string;
succeeded: boolean;
}
export type WorkflowRunLog = {
@@ -18,3 +21,12 @@ export type WorkflowRunLogOutput = {
content: string;
error: string;
};
export const WORKFLOW_RUN_STATUSES = Object.freeze({
PENDING: "pending",
RUNNING: "running",
SUCCEEDED: "succeeded",
FAILED: "failed",
} as const);
export type WorkflorRunStatusType = (typeof WORKFLOW_RUN_STATUSES)[keyof typeof WORKFLOW_RUN_STATUSES];

View File

@@ -16,7 +16,7 @@
"workflow.props.trigger": "Trigger",
"workflow.props.trigger.auto": "Auto",
"workflow.props.trigger.manual": "Manual",
"workflow.props.latest_execution_status": "Latest execution status",
"workflow.props.last_run_at": "Last run at",
"workflow.props.state": "State",
"workflow.props.state.filter.enabled": "Enabled",
"workflow.props.state.filter.disabled": "Disabled",

View File

@@ -1,9 +1,13 @@
{
"workflow_run.props.id": "ID",
"workflow_run.props.status": "Status",
"workflow_run.props.status.pending": "Pending",
"workflow_run.props.status.running": "Running",
"workflow_run.props.status.succeeded": "Succeeded",
"workflow_run.props.status.failed": "Failed",
"workflow_run.props.trigger": "Trigger",
"workflow_run.props.trigger.auto": "Timing",
"workflow_run.props.trigger.manual": "Manual",
"workflow_run.props.started_at": "Started at",
"workflow_run.props.completed_at": "Completed at"
"workflow_run.props.ended_at": "Ended at"
}

View File

@@ -16,7 +16,7 @@
"workflow.props.trigger": "触发方式",
"workflow.props.trigger.auto": "自动",
"workflow.props.trigger.manual": "手动",
"workflow.props.latest_execution_status": "最近执行状态",
"workflow.props.last_run_at": "最近执行时间",
"workflow.props.state": "启用状态",
"workflow.props.state.filter.enabled": "启用",
"workflow.props.state.filter.disabled": "未启用",

View File

@@ -1,9 +1,13 @@
{
"workflow_run.props.id": "ID",
"workflow_run.props.status": "状态",
"workflow_run.props.status.pending": "等待执行",
"workflow_run.props.status.running": "执行中",
"workflow_run.props.status.succeeded": "成功",
"workflow_run.props.status.failed": "失败",
"workflow_run.props.trigger": "触发方式",
"workflow_run.props.trigger": "执行方式",
"workflow_run.props.trigger.auto": "定时执行",
"workflow_run.props.trigger.manual": "手动执行",
"workflow_run.props.started_at": "开始时间",
"workflow_run.props.completed_at": "完成时间"
"workflow_run.props.ended_at": "完成时间"
}

View File

@@ -125,7 +125,7 @@ const CertificateList = () => {
}
}}
>
{record.expand?.workflowId?.name ?? `#${workflowId}`}
{record.expand?.workflowId?.name ?? <span className="font-mono">{t(`#${workflowId}`)}</span>}
</Typography.Link>
</Space>
);
@@ -174,7 +174,7 @@ const CertificateList = () => {
icon={<DeleteOutlinedIcon />}
variant="text"
onClick={() => {
alert("TODO");
alert("TODO: 暂时不支持删除证书");
}}
/>
</Tooltip>

View File

@@ -248,7 +248,7 @@ const WorkflowDetail = () => {
<Show when={tabValue === "orchestration"}>
<div className="relative">
<div className="py-12 lg:pr-36 xl:pr-48">
<WorkflowElements disabled={isRunning} />
<WorkflowElements />
</div>
<div className="absolute right-0 top-0 z-[1]">
<Space>

View File

@@ -1,10 +1,18 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import { DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon, PlusOutlined as PlusOutlinedIcon } from "@ant-design/icons";
import {
CheckCircleOutlined as CheckCircleOutlinedIcon,
CloseCircleOutlined as CloseCircleOutlinedIcon,
DeleteOutlined as DeleteOutlinedIcon,
EditOutlined as EditOutlinedIcon,
PlusOutlined as PlusOutlinedIcon,
} from "@ant-design/icons";
import { PageHeader } from "@ant-design/pro-components";
import { useRequest } from "ahooks";
import {
Badge,
Button,
Divider,
Empty,
@@ -26,6 +34,7 @@ import dayjs from "dayjs";
import { ClientResponseError } from "pocketbase";
import { WORKFLOW_TRIGGERS, type WorkflowModel, isAllNodesValidated } from "@/domain/workflow";
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
import { list as listWorkflow, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
import { getErrMsg } from "@/utils/error";
@@ -146,11 +155,28 @@ const WorkflowList = () => {
},
},
{
key: "lastExecutedAt",
title: t("workflow.props.latest_execution_status"),
render: () => {
// TODO: 最近执行状态
return <>TODO</>;
key: "lastRun",
title: t("workflow.props.last_run_at"),
render: (_, record) => {
if (record.lastRunId) {
if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.SUCCEEDED) {
return (
<Space className="max-w-full" direction="vertical" size={4}>
<Badge status="success" count={<CheckCircleOutlinedIcon style={{ color: themeToken.colorSuccess }} />} />
<Typography.Text type="secondary">{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
</Space>
);
} else if (record.lastRunStatus === WORKFLOW_RUN_STATUSES.FAILED) {
return (
<Space>
<Badge status="error" count={<CloseCircleOutlinedIcon style={{ color: themeToken.colorError }} />} />
<Typography.Text>{dayjs(record.lastRunTime!).format("YYYY-MM-DD HH:mm:ss")}</Typography.Text>
</Space>
);
}
}
return <></>;
},
},
{