diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx
index 860ef16e7..7371da4dd 100644
--- a/ui/src/components/workflow/WorkflowRunDetail.tsx
+++ b/ui/src/components/workflow/WorkflowRunDetail.tsx
@@ -99,13 +99,13 @@ const WorkflowRunDetail = ({ className, style, ...props }: WorkflowRunDetailProp
{t("workflow_run.logs")}
-
+
-
+ 0}>
{t("workflow_run.artifacts")}
-
+
@@ -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;
type LogGroup = { id: string; name: string; records: Log[] };
const [listData, setListData] = useState([]);
@@ -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["columns"] = [
{
key: "$index",
@@ -426,6 +430,7 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => {
const [tableData, setTableData] = useState([]);
const { loading } = useRequest(
() => {
+ // TODO: 目前输出产物只有证书
return listCertificatesByWorkflowRunId(runId);
},
{
diff --git a/ui/src/domain/workflowRun.ts b/ui/src/domain/workflowRun.ts
index 167d5f117..08a271c42 100644
--- a/ui/src/domain/workflowRun.ts
+++ b/ui/src/domain/workflowRun.ts
@@ -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;
};
diff --git a/ui/src/i18n/locales/en/nls.workflow.runs.json b/ui/src/i18n/locales/en/nls.workflow.runs.json
index 9115fe37a..df66ca266 100644
--- a/ui/src/i18n/locales/en/nls.workflow.runs.json
+++ b/ui/src/i18n/locales/en/nls.workflow.runs.json
@@ -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}}.",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.runs.json b/ui/src/i18n/locales/zh/nls.workflow.runs.json
index 4087fd1cf..993c482c0 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.runs.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.runs.json
@@ -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}}。",
diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx
index b847c3145..046723e68 100644
--- a/ui/src/pages/dashboard/Dashboard.tsx
+++ b/ui/src/pages/dashboard/Dashboard.tsx
@@ -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();
+ 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();
+ } else {
+ const KEY = "other";
+ if (keys.has(KEY)) continue;
+
+ keys.add(KEY);
+ icons.push();
+ }
+ }
+
+ return {icons}
;
+ }
+
return <>>;
},
},
diff --git a/ui/src/pages/workflows/WorkflowDetailRuns.tsx b/ui/src/pages/workflows/WorkflowDetailRuns.tsx
index da98776bf..e7ea0a24a 100644
--- a/ui/src/pages/workflows/WorkflowDetailRuns.tsx
+++ b/ui/src/pages/workflows/WorkflowDetailRuns.tsx
@@ -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();
+ 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();
+ } else {
+ const KEY = "other";
+ if (keys.has(KEY)) continue;
+
+ keys.add(KEY);
+ icons.push();
+ }
+ }
+
+ return {icons}
;
+ }
+
+ return <>>;
+ },
+ },
{
key: "$action",
align: "end",
diff --git a/ui/src/repository/workflowRun.ts b/ui/src/repository/workflowRun.ts
index 786703084..8610c2a8a 100644
--- a/ui/src/repository/workflowRun.ts
+++ b/ui/src/repository/workflowRun.ts
@@ -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(page, perPage, {
+ const list = await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).getList(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(id, {
expand: ["workflowRef"].join(","),
fields: ["*", ..._expandFields].join(","),
requestKey: null,
});
+ await enrichOutputs(record);
+ return record;
};
export const remove = async (record: MaybeModelRecordWithId | MaybeModelRecordWithId[]) => {
@@ -67,3 +71,31 @@ export const subscribe = async (id: string, cb: (e: RecordSubscription {
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;
+ }
+};