From 882f8025853449b9a581e04dc2c10936ac86bfe1 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 19 Mar 2025 10:09:30 +0800 Subject: [PATCH] feat(ui): enhance workflow logs display --- internal/domain/workflow_log.go | 5 +- .../huaweicloud-elb/huaweicloud_elb.go | 8 - internal/repository/workflow_log.go | 4 +- internal/workflow/dispatcher/invoker.go | 1 + .../workflow/node-processor/apply_node.go | 4 +- .../workflow/node-processor/deploy_node.go | 2 +- .../workflow/node-processor/upload_node.go | 2 +- migrations/1742209200_upgrade.go | 17 +- .../components/workflow/WorkflowRunDetail.tsx | 153 ++++++++++++++---- .../components/workflow/node/BranchNode.tsx | 14 +- .../workflow/node/ExecuteResultBranchNode.tsx | 14 +- ui/src/domain/workflowLog.ts | 1 + ui/src/i18n/locales/en/nls.workflow.runs.json | 4 +- ui/src/i18n/locales/zh/nls.workflow.runs.json | 4 +- ui/src/repository/certificate.ts | 2 +- ui/src/repository/workflowLog.ts | 2 +- 16 files changed, 168 insertions(+), 69 deletions(-) diff --git a/internal/domain/workflow_log.go b/internal/domain/workflow_log.go index a33c5480..05eef5a7 100644 --- a/internal/domain/workflow_log.go +++ b/internal/domain/workflow_log.go @@ -8,8 +8,9 @@ type WorkflowLog struct { Meta WorkflowId string `json:"workflowId" db:"workflowId"` RunId string `json:"workflorunIdwId" db:"runId"` - NodeId string `json:"nodeId"` - NodeName string `json:"nodeName"` + NodeId string `json:"nodeId" db:"nodeId"` + NodeName string `json:"nodeName" db:"nodeName"` + Timestamp int64 `json:"timestamp" db:"timestamp"` // 毫秒级时间戳 Level string `json:"level" db:"level"` Message string `json:"message" db:"message"` Data map[string]any `json:"data" db:"data"` diff --git a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go index 8783c053..618af762 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-elb/huaweicloud_elb.go @@ -90,14 +90,6 @@ func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { } func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { - // 上传证书到 SCM - upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) - if err != nil { - return nil, xerrors.Wrap(err, "failed to upload certificate file") - } else { - d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) - } - // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_CERTIFICATE: diff --git a/internal/repository/workflow_log.go b/internal/repository/workflow_log.go index 95bc2e7d..0b801231 100644 --- a/internal/repository/workflow_log.go +++ b/internal/repository/workflow_log.go @@ -22,7 +22,7 @@ func (r *WorkflowLogRepository) ListByWorkflowRunId(ctx context.Context, workflo records, err := app.GetApp().FindRecordsByFilter( domain.CollectionNameWorkflowLog, "runId={:runId}", - "-created", + "timestamp", 0, 0, dbx.Params{"runId": workflowRunId}, ) @@ -66,6 +66,7 @@ func (r *WorkflowLogRepository) Save(ctx context.Context, workflowLog *domain.Wo record.Set("runId", workflowLog.RunId) record.Set("nodeId", workflowLog.NodeId) record.Set("nodeName", workflowLog.NodeName) + record.Set("timestamp", workflowLog.Timestamp) record.Set("level", workflowLog.Level) record.Set("message", workflowLog.Message) record.Set("data", workflowLog.Data) @@ -102,6 +103,7 @@ func (r *WorkflowLogRepository) castRecordToModel(record *core.Record) (*domain. RunId: record.GetString("runId"), NodeId: record.GetString("nodeId"), NodeName: record.GetString("nodeName"), + Timestamp: int64(record.GetInt("timestamp")), Level: record.GetString("level"), Message: record.GetString("message"), Data: logdata, diff --git a/internal/workflow/dispatcher/invoker.go b/internal/workflow/dispatcher/invoker.go index 23d70f01..5f344458 100644 --- a/internal/workflow/dispatcher/invoker.go +++ b/internal/workflow/dispatcher/invoker.go @@ -80,6 +80,7 @@ func (w *workflowInvoker) processNode(ctx context.Context, node *domain.Workflow log.RunId = w.runId log.NodeId = current.Id log.NodeName = current.Name + log.Timestamp = record.Time.UnixMilli() log.Level = record.Level.String() log.Message = record.Message log.Data = record.Data diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 2fc6c223..dc9a95d1 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -42,7 +42,7 @@ func (n *applyNode) Process(ctx context.Context) error { // 检测是否可以跳过本次执行 if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { - n.logger.Warn(fmt.Sprintf("skip this application, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("skip this application, because %s", skipReason)) return nil } else if skipReason != "" { n.logger.Info(fmt.Sprintf("continue to apply, because %s", skipReason)) @@ -124,7 +124,7 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo renewalInterval := time.Duration(currentNodeConfig.SkipBeforeExpiryDays) * time.Hour * 24 expirationTime := time.Until(lastCertificate.ExpireAt) if expirationTime > renewalInterval { - return true, fmt.Sprintf("the certificate has already been issued (expires in %dD, next renewal in %dD)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) + return true, fmt.Sprintf("the certificate has already been issued (expires in %dd, next renewal in %dd)", int(expirationTime.Hours()/24), currentNodeConfig.SkipBeforeExpiryDays) } } } diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 95d99bfa..42bc9ca6 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -55,7 +55,7 @@ func (n *deployNode) Process(ctx context.Context) error { // 检测是否可以跳过本次执行 if lastOutput != nil && certificate.CreatedAt.Before(lastOutput.UpdatedAt) { if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { - n.logger.Warn(fmt.Sprintf("skip this deployment, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("skip this deployment, because %s", skipReason)) return nil } else if skipReason != "" { n.logger.Info(fmt.Sprintf("continue to deploy, because %s", skipReason)) diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index a3640c2d..6c46e90f 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -40,7 +40,7 @@ func (n *uploadNode) Process(ctx context.Context) error { // 检测是否可以跳过本次执行 if skippable, skipReason := n.checkCanSkip(ctx, lastOutput); skippable { - n.logger.Warn(fmt.Sprintf("skip this upload, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("skip this upload, because %s", skipReason)) return nil } else if skipReason != "" { n.logger.Info(fmt.Sprintf("continue to upload, because %s", skipReason)) diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go index 8c9ede5f..0a980972 100644 --- a/migrations/1742209200_upgrade.go +++ b/migrations/1742209200_upgrade.go @@ -3,6 +3,7 @@ package migrations import ( "encoding/json" "strings" + "time" "github.com/pocketbase/pocketbase/core" m "github.com/pocketbase/pocketbase/migrations" @@ -86,6 +87,18 @@ func init() { "system": false, "type": "text" }, + { + "hidden": false, + "id": "number2782324286", + "max": null, + "min": null, + "name": "timestamp", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, { "autogeneratePattern": "", "hidden": false, @@ -192,13 +205,15 @@ func init() { for _, log := range logs { for _, logRecord := range log.Records { record := core.NewRecord(collection) + createdAt, _ := time.Parse(time.RFC3339, logRecord.Time) record.Set("workflowId", workflowRun.Get("workflowId")) record.Set("runId", workflowRun.Get("id")) record.Set("nodeId", log.NodeId) record.Set("nodeName", log.NodeName) + record.Set("timestamp", createdAt.UnixMilli()) record.Set("level", logRecord.Level) record.Set("message", strings.TrimSpace(logRecord.Content+" "+logRecord.Error)) - record.Set("created", log.Records) + record.Set("created", createdAt) if err := app.Save(record); err != nil { return err } diff --git a/ui/src/components/workflow/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 5fbe5520..5d8c7f29 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -1,8 +1,34 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { RightOutlined as RightOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; +import { + CheckCircleOutlined as CheckCircleOutlinedIcon, + CheckOutlined as CheckOutlinedIcon, + ClockCircleOutlined as ClockCircleOutlinedIcon, + CloseCircleOutlined as CloseCircleOutlinedIcon, + RightOutlined as RightOutlinedIcon, + SelectOutlined as SelectOutlinedIcon, + SettingOutlined as SettingOutlinedIcon, + StopOutlined as StopOutlinedIcon, + SyncOutlined as SyncOutlinedIcon, +} from "@ant-design/icons"; import { useRequest } from "ahooks"; -import { Alert, Button, Collapse, Divider, Empty, Skeleton, Space, Spin, Table, type TableProps, Tooltip, Typography, notification } from "antd"; +import { + Button, + Collapse, + Divider, + Dropdown, + Empty, + Flex, + Skeleton, + Space, + Spin, + Table, + type TableProps, + Tooltip, + Typography, + notification, + theme, +} from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; @@ -23,25 +49,14 @@ export type WorkflowRunDetailProps = { }; const WorkflowRunDetail = ({ data, ...props }: WorkflowRunDetailProps) => { - const { t } = useTranslation(); - return (
- - {t("workflow_run.props.status.succeeded")}} /> - - - - {t("workflow_run.props.status.failed")}} /> - - -
+ -
+ - + -
@@ -51,9 +66,10 @@ 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 { token: themeToken } = theme.useToken(); + type Log = Pick; + type LogGroup = { id: string; name: string; records: Log[] }; const [listData, setListData] = useState([]); const { loading } = useRequest( () => { @@ -61,13 +77,12 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin }, { refreshDeps: [runId, runStatus], - pollingInterval: runStatus === WORKFLOW_RUN_STATUSES.PENDING || runStatus === WORKFLOW_RUN_STATUSES.RUNNING ? 5000 : 0, + pollingInterval: runStatus === WORKFLOW_RUN_STATUSES.PENDING || runStatus === WORKFLOW_RUN_STATUSES.RUNNING ? 3000 : 0, pollingWhenHidden: false, throttleWait: 500, - onBefore: () => { - setListData([]); - }, onSuccess: (res) => { + if (res.items.length === listData.flatMap((e) => e.records).length) return; + setListData( res.items.reduce((acc, e) => { let group = acc.at(-1); @@ -75,7 +90,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin 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 }); + group.records.push({ timestamp: e.timestamp, level: e.level, message: e.message, data: e.data }); return acc; }, [] as LogGroup[]) ); @@ -92,7 +107,52 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin } ); - const renderLogRecord = (record: Log) => { + const [showTimestamp, setShowTimestamp] = useState(true); + const [showWhitespace, setShowWhitespace] = useState(true); + + const renderBadge = () => { + switch (runStatus) { + case WORKFLOW_RUN_STATUSES.PENDING: + return ( + + + {t("workflow_run.props.status.pending")} + + ); + case WORKFLOW_RUN_STATUSES.RUNNING: + return ( + + + {t("workflow_run.props.status.running")} + + ); + case WORKFLOW_RUN_STATUSES.SUCCEEDED: + return ( + + + {t("workflow_run.props.status.succeeded")} + + ); + case WORKFLOW_RUN_STATUSES.FAILED: + return ( + + + {t("workflow_run.props.status.failed")} + + ); + case WORKFLOW_RUN_STATUSES.CANCELED: + return ( + + + {t("workflow_run.props.status.canceled")} + + ); + } + + return <>; + }; + + const renderRecord = (record: Log) => { let message = <>{record.message}; if (record.data != null && Object.keys(record.data).length > 0) { message = ( @@ -100,8 +160,8 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin {record.message} {Object.entries(record.data).map(([key, value]) => (
-
{key}:
-
{JSON.stringify(value)}
+
{key}:
+
{JSON.stringify(value)}
))} @@ -110,13 +170,14 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin return (
-
[{dayjs(record.created).format("YYYY-MM-DD HH:mm:ss")}]
+ {showTimestamp ?
[{dayjs(record.timestamp).format("YYYY-MM-DD HH:mm:ss")}]
: <>}
{message} @@ -129,6 +190,35 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin <> {t("workflow_run.logs")}
+
+
{renderBadge()}
+
+ , + onClick: () => setShowTimestamp(!showTimestamp), + }, + { + key: "show-whitespace", + label: t("workflow_run.logs.menu.show_whitespaces"), + icon: , + onClick: () => setShowWhitespace(!showWhitespace), + }, + ], + }} + trigger={["click"]} + > +
+
+ + + 0} fallback={ @@ -137,7 +227,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin } > -
+
{group.records.map((record) => renderLogRecord(record))}
, + children:
{group.records.map((record) => renderRecord(record))}
, }; })} /> @@ -221,9 +311,6 @@ const WorkflowRunArtifacts = ({ runId }: { runId: string }) => { }, { refreshDeps: [runId], - onBefore: () => { - setTableData([]); - }, onSuccess: (res) => { setTableData(res.items); }, diff --git a/ui/src/components/workflow/node/BranchNode.tsx b/ui/src/components/workflow/node/BranchNode.tsx index 4a68f315..f8a755d0 100644 --- a/ui/src/components/workflow/node/BranchNode.tsx +++ b/ui/src/components/workflow/node/BranchNode.tsx @@ -10,8 +10,6 @@ import AddNode from "./AddNode"; import WorkflowElement from "../WorkflowElement"; import { type SharedNodeProps } from "./_SharedNode"; -const { useToken } = theme; - export type BrandNodeProps = SharedNodeProps; const BranchNode = ({ node, disabled }: BrandNodeProps) => { @@ -19,7 +17,7 @@ const BranchNode = ({ node, disabled }: BrandNodeProps) => { const { addBranch } = useWorkflowStore(useZustandShallowSelector(["addBranch"])); - const token = useToken(); + const { token: themeToken } = theme.useToken(); const renderBranch = (node: WorkflowNode, branchNodeId?: string, branchIndex?: number) => { const elements: JSX.Element[] = []; @@ -38,7 +36,7 @@ const BranchNode = ({ node, disabled }: BrandNodeProps) => {