feat(ui): display workflow run artifacts in list

This commit is contained in:
Fu Diwei
2026-01-06 15:23:24 +08:00
committed by RHQYZ
parent ec13423b65
commit 1b390dd004
7 changed files with 119 additions and 10 deletions
@@ -99,13 +99,13 @@ const WorkflowRunDetail = ({ className, style, ...props }: WorkflowRunDetailProp
<div className="mt-8">
<Typography.Title level={5}>{t("workflow_run.logs")}</Typography.Title>
<WorkflowRunLogs runId={mergedData.id} runStatus={mergedData.status} />
<WorkflowRunLogs runData={mergedData} />
</div>
<Show when={mergedData.status === WORKFLOW_RUN_STATUSES.SUCCEEDED}>
<Show when={mergedData.outputs && mergedData.outputs.length > 0}>
<div className="mt-8">
<Typography.Title level={5}>{t("workflow_run.artifacts")}</Typography.Title>
<WorkflowRunArtifacts runId={mergedData.id} />
<WorkflowRunArtifacts runData={mergedData} />
</div>
</Show>
</div>
@@ -183,11 +183,13 @@ const WorkflowRunProcess = ({ runData }: { runData: WorkflowRunModel }) => {
);
};
const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: string }) => {
const WorkflowRunLogs = ({ runData }: { runData: WorkflowRunModel }) => {
const { t } = useTranslation();
const { theme: browserTheme } = useBrowserTheme();
const { id: runId, status: runStatus } = runData;
type Log = Pick<WorkflowLogModel, "timestamp" | "level" | "message" | "data">;
type LogGroup = { id: string; name: string; records: Log[] };
const [listData, setListData] = useState<LogGroup[]>([]);
@@ -197,7 +199,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
},
{
refreshDeps: [runId, runStatus],
pollingInterval: 1500,
pollingInterval: 1000,
pollingWhenHidden: false,
throttleWait: 500,
onSuccess: (res) => {
@@ -374,11 +376,13 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin
);
};
const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
const WorkflowRunArtifacts = ({ runData }: { runData: WorkflowRunModel }) => {
const { t } = useTranslation();
const { notification } = App.useApp();
const { id: runId } = runData;
const tableColumns: TableProps<CertificateModel>["columns"] = [
{
key: "$index",
@@ -426,6 +430,7 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
const [tableData, setTableData] = useState<CertificateModel[]>([]);
const { loading } = useRequest(
() => {
// TODO: 目前输出产物只有证书
return listCertificatesByWorkflowRunId(runId);
},
{
+6
View File
@@ -8,6 +8,12 @@ export interface WorkflowRunModel extends BaseModel {
endedAt: ISO8601String;
graph?: WorkflowGraph;
error?: string;
outputs?: Array<{
type: string;
name: string;
value: string;
valueType: string;
}>;
expand?: {
workflowRef?: Pick<WorkflowModel, "id" | "name" | "description">;
};
@@ -27,6 +27,7 @@
"workflow_run.props.trigger.manual": "Manual",
"workflow_run.props.started_at": "Started at",
"workflow_run.props.ended_at": "Ended at",
"workflow_run.props.artifacts": "Artifacts",
"workflow_run.base.description": "Triggered {{trigger}} at {{startedAt}}",
"workflow_run.base.description_with_time_cost": "Triggered {{trigger}} at {{startedAt}}. Time cost: {{timeCost}}.",
@@ -27,6 +27,7 @@
"workflow_run.props.trigger.manual": "手动",
"workflow_run.props.started_at": "开始时间",
"workflow_run.props.ended_at": "完成时间",
"workflow_run.props.artifacts": "输出产物",
"workflow_run.base.description": "{{trigger}}触发于 {{startedAt}}",
"workflow_run.base.description_with_time_cost": "{{trigger}}触发于 {{startedAt}},总计用时 {{timeCost}}。",
+33
View File
@@ -4,6 +4,8 @@ import { useNavigate } from "react-router-dom";
import {
IconActivity,
IconAlertHexagon,
IconBox,
IconCertificate,
IconCirclePlus,
IconConfetti,
IconExternalLink,
@@ -338,6 +340,37 @@ const WorkflowRunHistoryTable = ({ className, style }: { className?: string; sty
return dayjs(record.endedAt).format("YYYY-MM-DD HH:mm:ss");
}
return <></>;
},
},
{
key: "artifacts",
title: t("workflow_run.props.artifacts"),
width: 160,
render: (_, record) => {
if (record.outputs && record.outputs.length > 0) {
const keys = new Set<string>();
const icons: React.ReactNode[] = [];
for (const output of record.outputs) {
if (output.type === "ref" && output.value?.split("#")?.at(0) === "certificate") {
const KEY = "certificate";
if (keys.has(KEY)) continue;
keys.add(KEY);
icons.push(<IconCertificate key={KEY} size="1.25em" />);
} else {
const KEY = "other";
if (keys.has(KEY)) continue;
keys.add(KEY);
icons.push(<IconBox key={KEY} size="1.25em" />);
}
}
return <div className="flex items-center gap-2">{icons}</div>;
}
return <></>;
},
},
+32 -1
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { IconBrowserShare, IconDots, IconHistory, IconPlayerPause, IconTrash } from "@tabler/icons-react";
import { IconBox, IconBrowserShare, IconCertificate, IconDots, IconHistory, IconPlayerPause, IconTrash } from "@tabler/icons-react";
import { useRequest } from "ahooks";
import { App, Button, Dropdown, Skeleton, Table, type TableProps, theme } from "antd";
import dayjs from "dayjs";
@@ -88,6 +88,37 @@ const WorkflowDetailRuns = () => {
return <></>;
},
},
{
key: "artifacts",
title: t("workflow_run.props.artifacts"),
width: 160,
render: (_, record) => {
if (record.outputs && record.outputs.length > 0) {
const keys = new Set<string>();
const icons: React.ReactNode[] = [];
for (const output of record.outputs) {
if (output.type === "ref" && output.value?.split("#")?.at(0) === "certificate") {
const KEY = "certificate";
if (keys.has(KEY)) continue;
keys.add(KEY);
icons.push(<IconCertificate key={KEY} size="1.25em" />);
} else {
const KEY = "other";
if (keys.has(KEY)) continue;
keys.add(KEY);
icons.push(<IconBox key={KEY} size="1.25em" />);
}
}
return <div className="flex items-center gap-2">{icons}</div>;
}
return <></>;
},
},
{
key: "$action",
align: "end",
+35 -3
View File
@@ -2,7 +2,7 @@
import { type WorkflowRunModel } from "@/domain/workflowRun";
import { COLLECTION_NAME_WORKFLOW_RUN, getPocketBase } from "./_pocketbase";
import { COLLECTION_NAME_WORKFLOW_OUTPUT, COLLECTION_NAME_WORKFLOW_RUN, getPocketBase } from "./_pocketbase";
const _commonFields = ["id", "status", "trigger", "startedAt", "endedAt", "error", "created", "updated", "deleted"];
const _expandFields = ["expand.workflowRef.id", "expand.workflowRef.name", "expand.workflowRef.description"];
@@ -25,23 +25,27 @@ export const list = async ({
filters.push(pb.filter("workflowRef={:workflowId}", { workflowId: workflowId }));
}
return await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).getList<WorkflowRunModel>(page, perPage, {
const list = await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).getList<WorkflowRunModel>(page, perPage, {
expand: expand ? ["workflowRef"].join(",") : void 0,
fields: [..._commonFields, ..._expandFields].join(","),
filter: filters.join(" && "),
sort: "-created",
requestKey: null,
});
await enrichOutputs(list.items);
return list;
};
export const get = async (id: string) => {
return await getPocketBase()
const record = await getPocketBase()
.collection(COLLECTION_NAME_WORKFLOW_RUN)
.getOne<WorkflowRunModel>(id, {
expand: ["workflowRef"].join(","),
fields: ["*", ..._expandFields].join(","),
requestKey: null,
});
await enrichOutputs(record);
return record;
};
export const remove = async (record: MaybeModelRecordWithId<WorkflowRunModel> | MaybeModelRecordWithId<WorkflowRunModel>[]) => {
@@ -67,3 +71,31 @@ export const subscribe = async (id: string, cb: (e: RecordSubscription<WorkflowR
export const unsubscribe = async (id: string) => {
return getPocketBase().collection(COLLECTION_NAME_WORKFLOW_RUN).unsubscribe(id);
};
const enrichOutputs = async (records: WorkflowRunModel | WorkflowRunModel[]) => {
if (!Array.isArray(records)) {
records = [records];
}
const runIds = Array.from(new Set(records.map((e) => e.id)));
if (runIds.length === 0) {
return;
}
const pb = getPocketBase();
const list = await pb.collection(COLLECTION_NAME_WORKFLOW_OUTPUT).getFullList({
batch: 65535,
fields: ["id", "runRef", "outputs"].join(","),
filter: "(" + runIds.map((runId) => pb.filter("runRef={:runId}", { runId })).join(" || ") + ") && outputs!=null",
sort: "created",
requestKey: null,
});
for (const record of records) {
const outputs = list
.filter((e) => e.runRef === record.id)
.map((e) => e.outputs)
.flat();
record.outputs = outputs;
}
};