From 2d10fa0218deed4a9ee843c50e08fe533eac13b7 Mon Sep 17 00:00:00 2001 From: yoan <536464346@qq.com> Date: Wed, 20 Nov 2024 15:47:51 +0800 Subject: [PATCH] Save and display execution records --- internal/domain/certificate.go | 2 + internal/domain/workflow_run_log.go | 22 +++++ internal/repository/workflow.go | 16 ++++ internal/repository/workflow_output.go | 10 +- .../workflow/node-processor/apply_node.go | 19 +++- .../workflow/node-processor/condition_node.go | 3 +- .../workflow/node-processor/deploy_node.go | 22 ++++- internal/workflow/node-processor/processor.go | 27 ++---- .../workflow/node-processor/start_node.go | 3 +- .../node-processor/workflow_processor.go | 7 +- internal/workflow/service.go | 21 ++++- ui/src/components/workflow/DataTable.tsx | 29 +++--- ui/src/components/workflow/WorkflowLog.tsx | 92 +++++++++++++++++++ .../components/workflow/WorkflowLogDetail.tsx | 81 ++++++++++++++++ ui/src/domain/workflow.ts | 24 +++++ ui/src/pages/workflow/WorkflowDetail.tsx | 46 +++++++++- ui/src/pages/workflow/index.tsx | 6 +- ui/src/repository/workflow.ts | 29 +++++- 18 files changed, 405 insertions(+), 54 deletions(-) create mode 100644 internal/domain/workflow_run_log.go create mode 100644 ui/src/components/workflow/WorkflowLog.tsx create mode 100644 ui/src/components/workflow/WorkflowLogDetail.tsx diff --git a/internal/domain/certificate.go b/internal/domain/certificate.go index 740de8fc..894dd024 100644 --- a/internal/domain/certificate.go +++ b/internal/domain/certificate.go @@ -2,6 +2,8 @@ package domain import "time" +var ValidityDuration = time.Hour * 24 * 10 + type Certificate struct { Meta SAN string `json:"san"` diff --git a/internal/domain/workflow_run_log.go b/internal/domain/workflow_run_log.go new file mode 100644 index 00000000..fa77a9c9 --- /dev/null +++ b/internal/domain/workflow_run_log.go @@ -0,0 +1,22 @@ +package domain + +type RunLogOutput struct { + Time string `json:"time"` + Title string `json:"title"` + Content string `json:"content"` + Error string `json:"error"` +} + +type RunLog struct { + NodeName string `json:"nodeName"` + Error string `json:"error"` + Outputs []RunLogOutput `json:"outputs"` +} + +type WorkflowRunLog struct { + Meta + Workflow string `json:"workflow"` + Log []RunLog `json:"log"` + Succeed bool `json:"succeed"` + Error string `json:"error"` +} diff --git a/internal/repository/workflow.go b/internal/repository/workflow.go index b39d8ac0..00da85cf 100644 --- a/internal/repository/workflow.go +++ b/internal/repository/workflow.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" + "github.com/pocketbase/pocketbase/models" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/utils/app" ) @@ -15,6 +16,21 @@ func NewWorkflowRepository() *WorkflowRepository { return &WorkflowRepository{} } +func (w *WorkflowRepository) SaveRunLog(ctx context.Context, log *domain.WorkflowRunLog) error { + collection, err := app.GetApp().Dao().FindCollectionByNameOrId("workflow_run_log") + if err != nil { + return err + } + record := models.NewRecord(collection) + + record.Set("workflow", log.Workflow) + record.Set("log", log.Log) + record.Set("succeed", log.Succeed) + record.Set("error", log.Error) + + return app.GetApp().Dao().SaveRecord(record) +} + func (w *WorkflowRepository) Get(ctx context.Context, id string) (*domain.Workflow, error) { record, err := app.GetApp().Dao().FindRecordById("workflow", id) if err != nil { diff --git a/internal/repository/workflow_output.go b/internal/repository/workflow_output.go index 58930bac..cd9e0d47 100644 --- a/internal/repository/workflow_output.go +++ b/internal/repository/workflow_output.go @@ -43,8 +43,8 @@ func (w *WorkflowOutputRepository) Get(ctx context.Context, nodeId string) (*dom rs := &domain.WorkflowOutput{ Meta: domain.Meta{ Id: record.GetId(), - Created: record.GetTime("created"), - Updated: record.GetTime("updated"), + Created: record.GetCreated().Time(), + Updated: record.GetUpdated().Time(), }, Workflow: record.GetString("workflow"), NodeId: record.GetString("nodeId"), @@ -73,15 +73,15 @@ func (w *WorkflowOutputRepository) GetCertificate(ctx context.Context, nodeId st rs := &domain.Certificate{ Meta: domain.Meta{ Id: record.GetId(), - Created: record.GetTime("created"), - Updated: record.GetTime("updated"), + Created: record.GetDateTime("created").Time(), + Updated: record.GetDateTime("updated").Time(), }, Certificate: record.GetString("certificate"), PrivateKey: record.GetString("privateKey"), IssuerCertificate: record.GetString("issuerCertificate"), SAN: record.GetString("san"), Output: record.GetString("output"), - ExpireAt: record.GetTime("expireAt"), + ExpireAt: record.GetDateTime("expireAt").Time(), CertUrl: record.GetString("certUrl"), CertStableUrl: record.GetString("certStableUrl"), Workflow: record.GetString("workflow"), diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index d90b512b..8411f575 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -2,6 +2,7 @@ package nodeprocessor import ( "context" + "time" "github.com/usual2970/certimate/internal/applicant" "github.com/usual2970/certimate/internal/domain" @@ -45,8 +46,16 @@ func (a *applyNode) Run(ctx context.Context) error { } if output != nil && output.Succeed { - a.AddOutput(ctx, a.node.Name, "已申请过") - return nil + cert, err := a.outputRepo.GetCertificate(ctx, a.node.Id) + if err != nil { + a.AddOutput(ctx, a.node.Name, "获取证书失败", err.Error()) + return err + } + + if time.Until(cert.ExpireAt) > domain.ValidityDuration { + a.AddOutput(ctx, a.node.Name, "已申请过证书,且证书在有效期内") + return nil + } } // 获取Applicant @@ -65,12 +74,18 @@ func (a *applyNode) Run(ctx context.Context) error { a.AddOutput(ctx, a.node.Name, "申请成功") // 记录申请结果 + // 保持一个节点只有一个输出 + outputId := "" + if output != nil { + outputId = output.Id + } output = &domain.WorkflowOutput{ Workflow: GetWorkflowId(ctx), NodeId: a.node.Id, Node: a.node, Succeed: true, Output: a.node.Output, + Meta: domain.Meta{Id: outputId}, } cert, err := x509.ParseCertificateFromPEM(certificate.Certificate) diff --git a/internal/workflow/node-processor/condition_node.go b/internal/workflow/node-processor/condition_node.go index 2bebe9ac..742b3be9 100644 --- a/internal/workflow/node-processor/condition_node.go +++ b/internal/workflow/node-processor/condition_node.go @@ -4,7 +4,6 @@ import ( "context" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/xtime" ) type conditionNode struct { @@ -21,7 +20,7 @@ func NewConditionNode(node *domain.WorkflowNode) *conditionNode { // 条件节点没有任何操作 func (c *conditionNode) Run(ctx context.Context) error { - c.AddOutput(ctx, xtime.BeijingTimeStr(), + c.AddOutput(ctx, c.node.Name, "完成", ) diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 36d494f0..56da6fb3 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -32,10 +32,6 @@ func (d *deployNode) Run(ctx context.Context) error { d.AddOutput(ctx, d.node.Name, "查询部署记录失败", err.Error()) return err } - if output != nil && output.Succeed { - d.AddOutput(ctx, d.node.Name, "已部署过") - return nil - } // 获取部署对象 // 获取证书 certSource := d.node.GetConfigString("certificate") @@ -52,6 +48,15 @@ func (d *deployNode) Run(ctx context.Context) error { return err } + // 未部署过,开始部署 + // 部署过但是证书更新了,重新部署 + // 部署过且证书未更新,直接返回 + + if d.deployed(output) && cert.Created.Before(output.Updated) { + d.AddOutput(ctx, d.node.Name, "已部署过且证书未更新") + return nil + } + accessRepo := repository.NewAccessRepository() access, err := accessRepo.GetById(context.Background(), d.node.GetConfigString("access")) if err != nil { @@ -86,11 +91,16 @@ func (d *deployNode) Run(ctx context.Context) error { d.AddOutput(ctx, d.node.Name, "部署成功") // 记录部署结果 + outputId := "" + if output != nil { + outputId = output.Id + } output = &domain.WorkflowOutput{ Workflow: GetWorkflowId(ctx), NodeId: d.node.Id, Node: d.node, Succeed: true, + Meta: domain.Meta{Id: outputId}, } if err := d.outputRepo.Save(ctx, output, nil, nil); err != nil { @@ -102,3 +112,7 @@ func (d *deployNode) Run(ctx context.Context) error { return nil } + +func (d *deployNode) deployed(output *domain.WorkflowOutput) bool { + return output != nil && output.Succeed +} diff --git a/internal/workflow/node-processor/processor.go b/internal/workflow/node-processor/processor.go index 10d143e9..e3a62297 100644 --- a/internal/workflow/node-processor/processor.go +++ b/internal/workflow/node-processor/processor.go @@ -8,51 +8,38 @@ import ( "github.com/usual2970/certimate/internal/utils/xtime" ) -type RunLog struct { - NodeName string `json:"node_name"` - Err string `json:"err"` - Outputs []RunLogOutput `json:"outputs"` -} - -type RunLogOutput struct { - Time string `json:"time"` - Title string `json:"title"` - Content string `json:"content"` - Error string `json:"error"` -} - type NodeProcessor interface { Run(ctx context.Context) error - Log(ctx context.Context) *RunLog + Log(ctx context.Context) *domain.RunLog AddOutput(ctx context.Context, title, content string, err ...string) } type Logger struct { - log *RunLog + log *domain.RunLog } func NewLogger(node *domain.WorkflowNode) *Logger { return &Logger{ - log: &RunLog{ + log: &domain.RunLog{ NodeName: node.Name, - Outputs: make([]RunLogOutput, 0), + Outputs: make([]domain.RunLogOutput, 0), }, } } -func (l *Logger) Log(ctx context.Context) *RunLog { +func (l *Logger) Log(ctx context.Context) *domain.RunLog { return l.log } func (l *Logger) AddOutput(ctx context.Context, title, content string, err ...string) { - output := RunLogOutput{ + output := domain.RunLogOutput{ Time: xtime.BeijingTimeStr(), Title: title, Content: content, } if len(err) > 0 { output.Error = err[0] - l.log.Err = err[0] + l.log.Error = err[0] } l.log.Outputs = append(l.log.Outputs, output) } diff --git a/internal/workflow/node-processor/start_node.go b/internal/workflow/node-processor/start_node.go index e880a08d..682b77d6 100644 --- a/internal/workflow/node-processor/start_node.go +++ b/internal/workflow/node-processor/start_node.go @@ -4,7 +4,6 @@ import ( "context" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/xtime" ) type startNode struct { @@ -21,7 +20,7 @@ func NewStartNode(node *domain.WorkflowNode) *startNode { // 开始节点没有任何操作 func (s *startNode) Run(ctx context.Context) error { - s.AddOutput(ctx, xtime.BeijingTimeStr(), + s.AddOutput(ctx, s.node.Name, "完成", ) diff --git a/internal/workflow/node-processor/workflow_processor.go b/internal/workflow/node-processor/workflow_processor.go index 87669080..4602254f 100644 --- a/internal/workflow/node-processor/workflow_processor.go +++ b/internal/workflow/node-processor/workflow_processor.go @@ -8,15 +8,20 @@ import ( type workflowProcessor struct { workflow *domain.Workflow - logs []RunLog + logs []domain.RunLog } func NewWorkflowProcessor(workflow *domain.Workflow) *workflowProcessor { return &workflowProcessor{ workflow: workflow, + logs: make([]domain.RunLog, 0), } } +func (w *workflowProcessor) Log(ctx context.Context) []domain.RunLog { + return w.logs +} + func (w *workflowProcessor) Run(ctx context.Context) error { ctx = WithWorkflowId(ctx, w.workflow.Id) return w.runNode(ctx, w.workflow.Content) diff --git a/internal/workflow/service.go b/internal/workflow/service.go index 224546d4..446381d2 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -11,6 +11,7 @@ import ( type WorkflowRepository interface { Get(ctx context.Context, id string) (*domain.Workflow, error) + SaveRunLog(ctx context.Context, log *domain.WorkflowRunLog) error } type WorkflowService struct { @@ -43,11 +44,29 @@ func (s *WorkflowService) Run(ctx context.Context, req *domain.WorkflowRunReq) e processor := nodeprocessor.NewWorkflowProcessor(workflow) if err := processor.Run(ctx); err != nil { + log := &domain.WorkflowRunLog{ + Workflow: workflow.Id, + Log: processor.Log(ctx), + Succeed: false, + Error: err.Error(), + } + if err := s.repo.SaveRunLog(ctx, log); err != nil { + app.GetApp().Logger().Error("failed to save run log", "err", err) + } return fmt.Errorf("failed to run workflow: %w", err) } // 保存执行日志 - + + log := &domain.WorkflowRunLog{ + Workflow: workflow.Id, + Log: processor.Log(ctx), + Succeed: true, + } + if err := s.repo.SaveRunLog(ctx, log); err != nil { + app.GetApp().Logger().Error("failed to save run log", "err", err) + return err + } return nil } diff --git a/ui/src/components/workflow/DataTable.tsx b/ui/src/components/workflow/DataTable.tsx index 3e4194db..4fdfe233 100644 --- a/ui/src/components/workflow/DataTable.tsx +++ b/ui/src/components/workflow/DataTable.tsx @@ -3,23 +3,21 @@ import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, Paginati import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Button } from "../ui/button"; import { useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; interface DataTableProps { columns: ColumnDef[]; data: TData[]; pageCount: number; onPageChange?: (pageIndex: number, pageSize?: number) => Promise; + onRowClick?: (id: string) => void; } -export function DataTable({ columns, data, onPageChange, pageCount }: DataTableProps) { +export function DataTable({ columns, data, onPageChange, pageCount, onRowClick }: DataTableProps) { const [{ pageIndex, pageSize }, setPagination] = useState({ pageIndex: 0, pageSize: 10, }); - const navigate = useNavigate(); - const pagination = { pageIndex, pageSize, @@ -43,11 +41,11 @@ export function DataTable({ columns, data, }, [pageIndex]); const handleRowClick = (id: string) => { - navigate(`/workflow/detail?id=${id}`); + onRowClick?.(id); }; return ( - <> +
@@ -88,14 +86,19 @@ export function DataTable({ columns, data,
- - + {table.getCanPreviousPage() && ( + + )} + + {table.getCanNextPage && ( + + )}
- + ); } diff --git a/ui/src/components/workflow/WorkflowLog.tsx b/ui/src/components/workflow/WorkflowLog.tsx new file mode 100644 index 00000000..c60671bc --- /dev/null +++ b/ui/src/components/workflow/WorkflowLog.tsx @@ -0,0 +1,92 @@ +import { WorkflowRunLog } from "@/domain/workflow"; +import { logs } from "@/repository/workflow"; +import { ColumnDef } from "@tanstack/react-table"; +import { useState } from "react"; +import { DataTable } from "./DataTable"; +import { useSearchParams } from "react-router-dom"; +import { Check, X } from "lucide-react"; +import WorkflowLogDetail from "./WorkflowLogDetail"; + +const WorkflowLog = () => { + const [data, setData] = useState([]); + const [pageCount, setPageCount] = useState(0); + + const [searchParams] = useSearchParams(); + const id = searchParams.get("id"); + + const [open, setOpen] = useState(false); + const [selectedLog, setSelectedLog] = useState(); + + const fetchData = async (page: number, pageSize?: number) => { + const resp = await logs({ page: page, perPage: pageSize, id: id ?? "" }); + setData(resp.items); + setPageCount(resp.totalPages); + }; + + const columns: ColumnDef[] = [ + { + accessorKey: "succeed", + header: "状态", + cell: ({ row }) => { + const succeed: boolean = row.getValue("succeed"); + if (succeed) { + return ( +
+
+ +
+
通过
+
+ ); + } else { + return ( +
+
+ +
+
失败
+
+ ); + } + }, + }, + { + accessorKey: "error", + header: "原因", + cell: ({ row }) => { + let error: string = row.getValue("error"); + if (!error) { + error = ""; + } + return
{error}
; + }, + }, + { + accessorKey: "created", + header: "时间", + cell: ({ row }) => { + const date: string = row.getValue("created"); + return new Date(date).toLocaleString(); + }, + }, + ]; + + const handleRowClick = (id: string) => { + setOpen(true); + const log = data.find((item) => item.id === id); + setSelectedLog(log); + }; + return ( +
+
+
日志
+ +
+ + +
+ ); +}; + +export default WorkflowLog; + diff --git a/ui/src/components/workflow/WorkflowLogDetail.tsx b/ui/src/components/workflow/WorkflowLogDetail.tsx new file mode 100644 index 00000000..6e805504 --- /dev/null +++ b/ui/src/components/workflow/WorkflowLogDetail.tsx @@ -0,0 +1,81 @@ +import { WorkflowOutput, WorkflowRunLog, WorkflowRunLogItem } from "@/domain/workflow"; +import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "../ui/sheet"; +import { Check, X } from "lucide-react"; + +type WorkflowLogDetailProps = { + open: boolean; + onOpenChange: (open: boolean) => void; + log?: WorkflowRunLog; +}; +const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps) => { + return ( + + + + 日志 + + +
+ {log?.succeed ? ( +
+
+
+ +
+
成功
+
+ +
{new Date(log.created).toLocaleString()}
+
+ ) : ( +
+
+
+ +
+
失败
+
+ +
{log?.error}
+ +
{log?.created && new Date(log.created).toLocaleString()}
+
+ )} + +
+ {log?.log.map((item: WorkflowRunLogItem, i) => { + return ( +
+
{item.nodeName}
+
+ {item.outputs.map((output: WorkflowOutput) => { + return ( + <> +
+
[{output.time}]
+ {output.error ? ( + <> +
{output.error}
+ + ) : ( + <> +
{output.content}
+ + )} +
+ + ); + })} +
+
+ ); + })} +
+
+
+
+ ); +}; + +export default WorkflowLogDetail; + diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 308928e3..892476cc 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -3,6 +3,29 @@ import { nanoid } from "nanoid"; import i18n from "@/i18n"; import { deployTargets, KVType } from "./domain"; +export type WorkflowRunLog = { + id: string; + workflow: string; + log: WorkflowRunLogItem[]; + error: string; + succeed: boolean; + created: string; + updated: string; +}; + +export type WorkflowRunLogItem = { + nodeName: string; + error: string; + outputs: WorkflowOutput[]; +}; + +export type WorkflowOutput = { + time: string; + title: string; + content: string; + error: string; +}; + export type Workflow = { id: string; name: string; @@ -446,3 +469,4 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [ }, }, ]; + diff --git a/ui/src/pages/workflow/WorkflowDetail.tsx b/ui/src/pages/workflow/WorkflowDetail.tsx index 1fcc2f40..8077159d 100644 --- a/ui/src/pages/workflow/WorkflowDetail.tsx +++ b/ui/src/pages/workflow/WorkflowDetail.tsx @@ -7,12 +7,14 @@ import { useToast } from "@/components/ui/use-toast"; import End from "@/components/workflow/End"; import NodeRender from "@/components/workflow/NodeRender"; import WorkflowBaseInfoEditDialog from "@/components/workflow/WorkflowBaseInfoEditDialog"; +import WorkflowLog from "@/components/workflow/WorkflowLog"; import WorkflowProvider from "@/components/workflow/WorkflowProvider"; import { allNodesValidated, WorkflowNode } from "@/domain/workflow"; +import { cn } from "@/lib/utils"; import { useWorkflowStore, WorkflowState } from "@/providers/workflow"; import { ArrowLeft } from "lucide-react"; -import { useEffect, useMemo } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useShallow } from "zustand/shallow"; @@ -30,11 +32,17 @@ const WorkflowDetail = () => { // 从 url 中获取 workflowId const [searchParams] = useSearchParams(); + const [locId, setLocId] = useState(""); const id = searchParams.get("id"); + const [tab, setTab] = useState("workflow"); + useEffect(() => { console.log(id); init(id ?? ""); + if (id) { + setLocId(id); + } }, [id]); const navigate = useNavigate(); @@ -85,6 +93,13 @@ const WorkflowDetail = () => { save(); }; + const getTabCls = (tabName: string) => { + if (tab === tabName) { + return "text-primary border-primary"; + } + return "border-transparent hover:text-primary hover:border-b-primary"; + }; + return ( <> @@ -102,6 +117,26 @@ const WorkflowDetail = () => { } /> + +
+
{ + setTab("workflow"); + }} + > +
流程
+
+
{ + setTab("history"); + }} + > +
历史
+
+
+
立即执行}> @@ -114,8 +149,15 @@ const WorkflowDetail = () => {
+ +
{elements}
+
-
{elements}
+ +
+ +
+
diff --git a/ui/src/pages/workflow/index.tsx b/ui/src/pages/workflow/index.tsx index 9aadc064..ca928870 100644 --- a/ui/src/pages/workflow/index.tsx +++ b/ui/src/pages/workflow/index.tsx @@ -185,6 +185,10 @@ const Workflow = () => { const handleCreateClick = () => { navigate("/workflow/detail"); }; + + const handleRowClick = (id: string) => { + navigate(`/workflow/detail?id=${id}`); + }; return ( <>
@@ -196,7 +200,7 @@ const Workflow = () => {
- +
{ @@ -39,3 +39,30 @@ export const list = async (req: WorkflowListReq) => { export const remove = async (id: string) => { return await getPb().collection("workflow").delete(id); }; + +type WorkflowLogsReq = { + id: string; + page: number; + perPage?: number; +}; + +export const logs = async (req: WorkflowLogsReq) => { + let page = 1; + if (req.page) { + page = req.page; + } + let perPage = 10; + if (req.perPage) { + perPage = req.perPage; + } + + const response = await getPb() + .collection("workflow_run_log") + .getList(page, perPage, { + sort: "-created", + filter: getPb().filter("workflow={:workflowId}", { workflowId: req.id }), + }); + + return response; +}; +