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; + } +};