mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-01 15:39:35 +08:00
feat(ui): display workflow run artifacts in list
This commit is contained in:
@@ -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);
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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}}。",
|
||||
|
||||
@@ -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 <></>;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user