From fd6e41c566fac67f9ceb97ed8e25ccd4b3d67df8 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 18 Mar 2025 20:02:39 +0800 Subject: [PATCH] feat(ui): workflow logs --- internal/deployer/deployer.go | 4 +- .../workflow/node-processor/deploy_node.go | 2 +- .../components/workflow/WorkflowRunDetail.tsx | 152 +++++++++++++++--- ui/src/domain/workflowLog.ts | 7 + ui/src/domain/workflowRun.ts | 17 +- ui/src/i18n/locales/en/nls.dashboard.json | 2 +- ui/src/i18n/locales/zh/nls.dashboard.json | 2 +- ui/src/pages/certificates/CertificateList.tsx | 6 +- ui/src/pages/dashboard/Dashboard.tsx | 2 +- ui/src/pages/workflows/WorkflowList.tsx | 4 +- ui/src/repository/_pocketbase.ts | 1 + ui/src/repository/certificate.ts | 6 +- ui/src/repository/workflow.ts | 4 +- ui/src/repository/workflowLog.ts | 19 +++ ui/src/repository/workflowRun.ts | 4 +- ui/src/stores/access/index.ts | 4 +- 16 files changed, 173 insertions(+), 63 deletions(-) create mode 100644 ui/src/domain/workflowLog.ts create mode 100644 ui/src/repository/workflowLog.ts diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 3a892404..36e92866 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -54,7 +54,6 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct { } return &proxyDeployer{ - logger: slog.Default(), deployer: deployer, deployCertificate: certdata.Certificate, deployPrivateKey: certdata.PrivateKey, @@ -63,7 +62,6 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct { // TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑 type proxyDeployer struct { - logger *slog.Logger deployer deployer.Deployer deployCertificate string deployPrivateKey string @@ -74,7 +72,7 @@ func (d *proxyDeployer) SetLogger(logger *slog.Logger) { panic("logger is nil") } - d.logger = logger + d.deployer.WithLogger(logger) } func (d *proxyDeployer) Deploy(ctx context.Context) error { diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 556ca891..95d99bfa 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -92,7 +92,7 @@ func (n *deployNode) Process(ctx context.Context) error { return err } - n.logger.Info("apply completed") + n.logger.Info("deploy completed") return nil } diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 785624f9..5fbe5520 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -1,16 +1,19 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; +import { RightOutlined as RightOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; import { useRequest } from "ahooks"; -import { Alert, Button, Divider, Empty, Space, Table, type TableProps, Tooltip, Typography, notification } from "antd"; +import { Alert, Button, Collapse, Divider, Empty, Skeleton, Space, Spin, Table, type TableProps, Tooltip, Typography, notification } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import Show from "@/components/Show"; import { type CertificateModel } from "@/domain/certificate"; +import type { WorkflowLogModel } from "@/domain/workflowLog"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; -import { listByWorkflowRunId as listCertificateByWorkflowRunId } from "@/repository/certificate"; +import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate"; +import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/workflowLog"; +import { mergeCls } from "@/utils/css"; import { getErrMsg } from "@/utils/error"; export type WorkflowRunDetailProps = { @@ -33,28 +36,7 @@ const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => {
- {t("workflow_run.logs")} -
-
- {data.logs?.map((item, i) => { - return ( -
-
{item.nodeName}
-
- {item.records?.map((output, j) => { - return ( -
-
[{dayjs(output.time).format("YYYY-MM-DD HH:mm:ss")}]
- {output.error ?
{output.error}
:
{output.content}
} -
- ); - })} -
-
- ); - })} -
-
+
@@ -66,6 +48,124 @@ const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => { ); }; +const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: string }) => { + const { t } = useTranslation(); + + type Log = Pick; + type LogGroup = { id: string; name: string; records: Log[] }; + + const [listData, setListData] = useState([]); + const { loading } = useRequest( + () => { + return listLogsByWorkflowRunId(runId); + }, + { + refreshDeps: [runId, runStatus], + pollingInterval: runStatus === WORKFLOW_RUN_STATUSES.PENDING || runStatus === WORKFLOW_RUN_STATUSES.RUNNING ? 5000 : 0, + pollingWhenHidden: false, + throttleWait: 500, + onBefore: () => { + setListData([]); + }, + onSuccess: (res) => { + setListData( + res.items.reduce((acc, e) => { + let group = acc.at(-1); + if (!group || group.id !== e.nodeId) { + group = { id: e.nodeId, name: e.nodeName, records: [] }; + acc.push(group); + } + group.records.push({ level: e.level, message: e.message, data: e.data, created: e.created }); + return acc; + }, [] as LogGroup[]) + ); + }, + onError: (err) => { + if (err instanceof ClientResponseError && err.isAbort) { + return; + } + + console.error(err); + + throw err; + }, + } + ); + + const renderLogRecord = (record: Log) => { + let message = <>{record.message}; + if (record.data != null && Object.keys(record.data).length > 0) { + message = ( +
+ {record.message} + {Object.entries(record.data).map(([key, value]) => ( +
+
{key}:
+
{JSON.stringify(value)}
+
+ ))} +
+ ); + } + + return ( +
+
[{dayjs(record.created).format("YYYY-MM-DD HH:mm:ss")}]
+
+ {message} +
+
+ ); + }; + + return ( + <> + {t("workflow_run.logs")} +
+ 0} + fallback={ + + + + } + > +
+ group.id)} + expandIcon={({ isActive }) => } + items={listData.map((group) => { + return { + key: group.id, + classNames: { + header: "text-sm text-stone-200", + body: "text-stone-200", + }, + style: { color: "inherit", border: "none" }, + styles: { + header: { color: "inherit" }, + }, + label: group.name, + children:
{group.records.map((record) => renderLogRecord(record))}
, + }; + })} + /> +
+
+
+ + ); +}; + const WorkflowRunArtifacts = ({ runId }: { runId: string }) => { const { t } = useTranslation(); @@ -117,7 +217,7 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => { const [tableData, setTableData] = useState([]); const { loading: tableLoading } = useRequest( () => { - return listCertificateByWorkflowRunId(runId); + return listCertificatesByWorkflowRunId(runId); }, { refreshDeps: [runId], diff --git a/ui/src/domain/workflowLog.ts b/ui/src/domain/workflowLog.ts new file mode 100644 index 00000000..ffe6fdd8 --- /dev/null +++ b/ui/src/domain/workflowLog.ts @@ -0,0 +1,7 @@ +export interface WorkflowLogModel extends Omit { + nodeId: string; + nodeName: string; + level: "DEBUG" | "INFO" | "WARN" | "ERROR"; + message: string; + data: Record; +} diff --git a/ui/src/domain/workflowRun.ts b/ui/src/domain/workflowRun.ts index 80872b31..6df4a406 100644 --- a/ui/src/domain/workflowRun.ts +++ b/ui/src/domain/workflowRun.ts @@ -6,27 +6,12 @@ export interface WorkflowRunModel extends BaseModel { trigger: string; startedAt: ISO8601String; endedAt: ISO8601String; - logs?: WorkflowRunLog[]; error?: string; expand?: { - workflowId?: WorkflowModel; + workflowId?: WorkflowModel; // TODO: ugly, maybe to use an alias? }; } -export type WorkflowRunLog = { - nodeId: string; - nodeName: string; - records?: WorkflowRunLogRecord[]; - error?: string; -}; - -export type WorkflowRunLogRecord = { - time: ISO8601String; - level: string; - content: string; - error?: string; -}; - export const WORKFLOW_RUN_STATUSES = Object.freeze({ PENDING: "pending", RUNNING: "running", diff --git a/ui/src/i18n/locales/en/nls.dashboard.json b/ui/src/i18n/locales/en/nls.dashboard.json index 8ae9d94d..38e20e1b 100644 --- a/ui/src/i18n/locales/en/nls.dashboard.json +++ b/ui/src/i18n/locales/en/nls.dashboard.json @@ -8,7 +8,7 @@ "dashboard.statistics.enabled_workflows": "Enabled workflows", "dashboard.statistics.unit": "", - "dashboard.latest_workflow_run": "Latest workflow run", + "dashboard.latest_workflow_runs": "Latest workflow runs", "dashboard.quick_actions": "Quick actions", "dashboard.quick_actions.create_workflow": "Create workflow", diff --git a/ui/src/i18n/locales/zh/nls.dashboard.json b/ui/src/i18n/locales/zh/nls.dashboard.json index badd7cb8..30eb5972 100644 --- a/ui/src/i18n/locales/zh/nls.dashboard.json +++ b/ui/src/i18n/locales/zh/nls.dashboard.json @@ -8,7 +8,7 @@ "dashboard.statistics.enabled_workflows": "已启用工作流", "dashboard.statistics.unit": "个", - "dashboard.latest_workflow_run": "最近执行的工作流", + "dashboard.latest_workflow_runs": "最近执行的工作流", "dashboard.quick_actions": "快捷操作", "dashboard.quick_actions.create_workflow": "新建工作流", diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index 265f0185..049069a3 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -28,7 +28,7 @@ import { ClientResponseError } from "pocketbase"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate"; -import { type ListCertificateRequest, list as listCertificate, remove as removeCertificate } from "@/repository/certificate"; +import { list as listCertificates, type ListRequest as listCertificatesRequest, remove as removeCertificate } from "@/repository/certificate"; import { getErrMsg } from "@/utils/error"; const CertificateList = () => { @@ -223,9 +223,9 @@ const CertificateList = () => { run: refreshData, } = useRequest( () => { - return listCertificate({ + return listCertificates({ keyword: filters["keyword"] as string, - state: filters["state"] as ListCertificateRequest["state"], + state: filters["state"] as listCertificatesRequest["state"], page: page, perPage: pageSize, }); diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index ea7a21cb..83f8cd47 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -275,7 +275,7 @@ const Dashboard = () => { - + columns={tableColumns} dataSource={tableData} diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 18a8b577..09bca7fc 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -41,7 +41,7 @@ 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 { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow"; import { getErrMsg } from "@/utils/error"; const WorkflowList = () => { @@ -253,7 +253,7 @@ const WorkflowList = () => { run: refreshData, } = useRequest( () => { - return listWorkflow({ + return listWorkflows({ keyword: filters["keyword"] as string, enabled: (filters["state"] as string) === "enabled" ? true : (filters["state"] as string) === "disabled" ? false : undefined, page: page, diff --git a/ui/src/repository/_pocketbase.ts b/ui/src/repository/_pocketbase.ts index 983c4987..85068f50 100644 --- a/ui/src/repository/_pocketbase.ts +++ b/ui/src/repository/_pocketbase.ts @@ -14,3 +14,4 @@ export const COLLECTION_NAME_SETTINGS = "settings"; export const COLLECTION_NAME_WORKFLOW = "workflow"; export const COLLECTION_NAME_WORKFLOW_RUN = "workflow_run"; export const COLLECTION_NAME_WORKFLOW_OUTPUT = "workflow_output"; +export const COLLECTION_NAME_WORKFLOW_LOG = "workflow_logs"; diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index b6b8d55e..f7c95f7d 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -3,14 +3,14 @@ import dayjs from "dayjs"; import { type CertificateModel } from "@/domain/certificate"; import { COLLECTION_NAME_CERTIFICATE, getPocketBase } from "./_pocketbase"; -export type ListCertificateRequest = { +export type ListRequest = { keyword?: string; state?: "expireSoon" | "expired"; page?: number; perPage?: number; }; -export const list = async (request: ListCertificateRequest) => { +export const list = async (request: ListRequest) => { const pb = getPocketBase(); const filters: string[] = ["deleted=null"]; @@ -39,7 +39,7 @@ export const listByWorkflowRunId = async (workflowRunId: string) => { const list = await pb.collection(COLLECTION_NAME_CERTIFICATE).getFullList({ batch: 65535, filter: pb.filter("workflowRunId={:workflowRunId}", { workflowRunId: workflowRunId }), - sort: "-created", + // sort: "created", requestKey: null, }); diff --git a/ui/src/repository/workflow.ts b/ui/src/repository/workflow.ts index 0b35a5e2..5701927c 100644 --- a/ui/src/repository/workflow.ts +++ b/ui/src/repository/workflow.ts @@ -3,14 +3,14 @@ import { type RecordSubscription } from "pocketbase"; import { type WorkflowModel } from "@/domain/workflow"; import { COLLECTION_NAME_WORKFLOW, getPocketBase } from "./_pocketbase"; -export type ListWorkflowRequest = { +export type ListRequest = { keyword?: string; enabled?: boolean; page?: number; perPage?: number; }; -export const list = async (request: ListWorkflowRequest) => { +export const list = async (request: ListRequest) => { const pb = getPocketBase(); const filters: string[] = []; diff --git a/ui/src/repository/workflowLog.ts b/ui/src/repository/workflowLog.ts new file mode 100644 index 00000000..a866d624 --- /dev/null +++ b/ui/src/repository/workflowLog.ts @@ -0,0 +1,19 @@ +import { type WorkflowLogModel } from "@/domain/workflowLog"; + +import { COLLECTION_NAME_WORKFLOW_LOG, getPocketBase } from "./_pocketbase"; + +export const listByWorkflowRunId = async (workflowRunId: string) => { + const pb = getPocketBase(); + + const list = await pb.collection(COLLECTION_NAME_WORKFLOW_LOG).getFullList({ + batch: 65535, + filter: pb.filter("runId={:runId}", { runId: workflowRunId }), + // sort: "created", + requestKey: null, + }); + + return { + totalItems: list.length, + items: list, + }; +}; diff --git a/ui/src/repository/workflowRun.ts b/ui/src/repository/workflowRun.ts index 51038f18..22c69802 100644 --- a/ui/src/repository/workflowRun.ts +++ b/ui/src/repository/workflowRun.ts @@ -4,14 +4,14 @@ import { type WorkflowRunModel } from "@/domain/workflowRun"; import { COLLECTION_NAME_WORKFLOW_RUN, getPocketBase } from "./_pocketbase"; -export type ListWorkflowRunsRequest = { +export type ListRequest = { workflowId?: string; page?: number; perPage?: number; expand?: boolean; }; -export const list = async (request: ListWorkflowRunsRequest) => { +export const list = async (request: ListRequest) => { const pb = getPocketBase(); const filters: string[] = []; diff --git a/ui/src/stores/access/index.ts b/ui/src/stores/access/index.ts index 61601978..55d8835a 100644 --- a/ui/src/stores/access/index.ts +++ b/ui/src/stores/access/index.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { type AccessModel } from "@/domain/access"; -import { list as listAccess, remove as removeAccess, save as saveAccess } from "@/repository/access"; +import { list as listAccesses, remove as removeAccess, save as saveAccess } from "@/repository/access"; export interface AccessesState { accesses: AccessModel[]; @@ -24,7 +24,7 @@ export const useAccessesStore = create((set) => { loadedAtOnce: false, fetchAccesses: async () => { - fetcher ??= listAccess().then((res) => res.items); + fetcher ??= listAccesses().then((res) => res.items); try { set({ loading: true });