certificate display

This commit is contained in:
yoan 2024-11-21 13:17:39 +08:00
parent 2d10fa0218
commit 09e4b24445
15 changed files with 331 additions and 45 deletions

View File

@ -20,3 +20,14 @@ type WorkflowRunLog struct {
Succeed bool `json:"succeed"`
Error string `json:"error"`
}
type RunLogs []RunLog
func (r RunLogs) Error() string {
for _, log := range r {
if log.Error != "" {
return log.Error
}
}
return ""
}

View File

@ -2,6 +2,7 @@ package nodeprocessor
import (
"context"
"strings"
"time"
"github.com/usual2970/certimate/internal/applicant"
@ -95,7 +96,7 @@ func (a *applyNode) Run(ctx context.Context) error {
}
certificateRecord := &domain.Certificate{
SAN: cert.Subject.CommonName,
SAN: strings.Join(cert.DNSNames, ";"),
Certificate: certificate.Certificate,
PrivateKey: certificate.PrivateKey,
IssuerCertificate: certificate.IssuerCertificate,

View File

@ -33,7 +33,7 @@ func (w *workflowProcessor) runNode(ctx context.Context, node *domain.WorkflowNo
if current.Type == domain.WorkflowNodeTypeBranch {
for _, branch := range current.Branches {
if err := w.runNode(ctx, &branch); err != nil {
continue
return err
}
}
}

View File

@ -57,11 +57,18 @@ func (s *WorkflowService) Run(ctx context.Context, req *domain.WorkflowRunReq) e
}
// 保存执行日志
logs := processor.Log(ctx)
runLogs := domain.RunLogs(logs)
runErr := runLogs.Error()
succeed := true
if runErr != "" {
succeed = false
}
log := &domain.WorkflowRunLog{
Workflow: workflow.Id,
Log: processor.Log(ctx),
Succeed: true,
Error: runErr,
Succeed: succeed,
}
if err := s.repo.SaveRunLog(ctx, log); err != nil {
app.GetApp().Logger().Error("failed to save run log", "err", err)

View File

@ -0,0 +1,63 @@
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "../ui/sheet";
import { Certificate } from "@/domain/certificate";
import { Textarea } from "../ui/textarea";
import { Button } from "../ui/button";
import { Label } from "../ui/label";
import { CustomFile, saveFiles2ZIP } from "@/lib/file";
type WorkflowLogDetailProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
certificate?: Certificate;
};
const CertificateDetail = ({ open, onOpenChange, certificate }: WorkflowLogDetailProps) => {
const handleDownloadClick = async () => {
const zipName = `${certificate?.id}-${certificate?.san}.zip`;
const files: CustomFile[] = [
{
name: `${certificate?.san}.pem`,
content: certificate?.certificate ? certificate?.certificate : "",
},
{
name: `${certificate?.san}.key`,
content: certificate?.privateKey ? certificate?.privateKey : "",
},
];
await saveFiles2ZIP(zipName, files);
};
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="sm:max-w-2xl">
<SheetHeader>
<SheetTitle></SheetTitle>
</SheetHeader>
<div className="flex flex-col space-y-5 mt-9">
<div className="flex justify-end">
<Button
size={"sm"}
onClick={() => {
handleDownloadClick();
}}
>
</Button>
</div>
<div className="flex flex-col space-y-3">
<Label></Label>
<Textarea value={certificate?.certificate} rows={10} readOnly={true} />
</div>
<div className="flex flex-col space-y-3">
<Label></Label>
<Textarea value={certificate?.privateKey} rows={10} readOnly={true} />
</div>
</div>
</SheetContent>
</Sheet>
);
};
export default CertificateDetail;

View File

@ -31,7 +31,7 @@ const WorkflowLog = () => {
const succeed: boolean = row.getValue("succeed");
if (succeed) {
return (
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 min-w-[150px]">
<div className="text-white bg-green-500 w-8 h-8 rounded-full flex items-center justify-center">
<Check size={18} />
</div>
@ -40,7 +40,7 @@ const WorkflowLog = () => {
);
} else {
return (
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 min-w-[150px]">
<div className="text-white bg-red-500 w-8 h-8 rounded-full flex items-center justify-center">
<X size={18} />
</div>
@ -58,7 +58,7 @@ const WorkflowLog = () => {
if (!error) {
error = "";
}
return <div className="min-w-[250px] truncate">{error}</div>;
return <div className="max-w-[300px] truncate text-red-500">{error}</div>;
},
},
{
@ -89,4 +89,3 @@ const WorkflowLog = () => {
};
export default WorkflowLog;

View File

@ -1,6 +1,7 @@
import { WorkflowOutput, WorkflowRunLog, WorkflowRunLogItem } from "@/domain/workflow";
import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "../ui/sheet";
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "../ui/sheet";
import { Check, X } from "lucide-react";
import { ScrollArea } from "../ui/scroll-area";
type WorkflowLogDetailProps = {
open: boolean;
@ -18,7 +19,7 @@ const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps)
<div className="flex flex-col">
{log?.succeed ? (
<div className="mt-5 flex justify-between bg-green-100 p-5 rounded-md items-center">
<div className="flex space-x-2 items-center">
<div className="flex space-x-2 items-center min-w-[150px]">
<div className="w-8 h-8 bg-green-500 flex items-center justify-center rounded-full text-white">
<Check size={18} />
</div>
@ -28,49 +29,51 @@ const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps)
<div className="text-muted-foreground">{new Date(log.created).toLocaleString()}</div>
</div>
) : (
<div className="mt-5 flex justify-between bg-green-100 p-5 rounded-md items-center">
<div className="flex space-x-2 items-center">
<div className="mt-5 flex justify-between bg-red-100 p-5 rounded-md items-center">
<div className="flex space-x-2 items-center min-w-[150px]">
<div className="w-8 h-8 bg-red-500 flex items-center justify-center rounded-full text-white">
<X size={18} />
</div>
<div className="text-stone-700"></div>
</div>
<div className="text-red-500">{log?.error}</div>
<div className="text-red-500 max-w-[400px] truncate">{log?.error}</div>
<div className="text-muted-foreground">{log?.created && new Date(log.created).toLocaleString()}</div>
</div>
)}
<div className="bg-black p-5 mt-5 rounded-md text-stone-200 flex flex-col space-y-3">
{log?.log.map((item: WorkflowRunLogItem, i) => {
return (
<div key={i} className="flex flex-col space-y-2">
<div className="">{item.nodeName}</div>
<div className="flex flex-col space-y-1">
{item.outputs.map((output: WorkflowOutput) => {
return (
<>
<div className="flex text-sm space-x-2">
<div>[{output.time}]</div>
{output.error ? (
<>
<div className="text-red-500">{output.error}</div>
</>
) : (
<>
<div>{output.content}</div>
</>
)}
</div>
</>
);
})}
<ScrollArea className="h-[80vh] mt-5 bg-black p-5 rounded-md">
<div className=" text-stone-200 flex flex-col space-y-3">
{log?.log.map((item: WorkflowRunLogItem, i) => {
return (
<div key={i} className="flex flex-col space-y-2">
<div className="">{item.nodeName}</div>
<div className="flex flex-col space-y-1">
{item.outputs.map((output: WorkflowOutput) => {
return (
<>
<div className="flex text-sm space-x-2">
<div>[{output.time}]</div>
{output.error ? (
<>
<div className="text-red-500 max-w-[70%]">{output.error}</div>
</>
) : (
<>
<div>{output.content}</div>
</>
)}
</div>
</>
);
})}
</div>
</div>
</div>
);
})}
</div>
);
})}
</div>
</ScrollArea>
</div>
</SheetContent>
</Sheet>
@ -78,4 +81,3 @@ const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps)
};
export default WorkflowLogDetail;

View File

@ -0,0 +1,21 @@
import { Workflow } from "./workflow";
export type Certificate = {
id: string;
san: string;
certificate: string;
privateKey: string;
issuerCertificate: string;
certUrl: string;
certStableUrl: string;
output: string;
expireAt: string;
workflow: string;
nodeId: string;
created: string;
updated: string;
expand: {
workflow?: Workflow;
};
};

View File

@ -469,4 +469,3 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [
},
},
];

View File

@ -31,6 +31,14 @@ export const getLeftDays = (zuluTime: string) => {
return days;
};
export const diffDays = (date1: string, date2: string) => {
const target1 = new Date(date1);
const target2 = new Date(date2);
const diff = target1.getTime() - target2.getTime();
const days = Math.ceil(diff / (1000 * 60 * 60 * 24));
return days;
};
export function getTimeBefore(days: number): string {
// 获取当前时间
const currentDate = new Date();

View File

@ -0,0 +1,141 @@
import CertificateDetail from "@/components/certificate/CertificateDetail";
import { Button } from "@/components/ui/button";
import { DataTable } from "@/components/workflow/DataTable";
import { Certificate as CertificateType } from "@/domain/certificate";
import { diffDays, getLeftDays } from "@/lib/time";
import { list } from "@/repository/certificate";
import { ColumnDef } from "@tanstack/react-table";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const Certificate = () => {
const [data, setData] = useState<CertificateType[]>([]);
const [pageCount, setPageCount] = useState<number>(0);
const [open, setOpen] = useState(false);
const [selectedCertificate, setSelectedCertificate] = useState<CertificateType>();
const fetchData = async (page: number, pageSize?: number) => {
const resp = await list({ page: page, perPage: pageSize });
setData(resp.items);
setPageCount(resp.totalPages);
};
const navigate = useNavigate();
const columns: ColumnDef<CertificateType>[] = [
{
accessorKey: "san",
header: "域名",
cell: ({ row }) => {
let san: string = row.getValue("san");
if (!san) {
san = "";
}
return (
<div>
{san.split(";").map((item, i) => {
return (
<div key={i} className="max-w-[250px] truncate">
{item}
</div>
);
})}
</div>
);
},
},
{
accessorKey: "expireAt",
header: "有效期限",
cell: ({ row }) => {
const expireAt: string = row.getValue("expireAt");
const data = row.original;
const leftDays = getLeftDays(expireAt);
const allDays = diffDays(data.expireAt, data.created);
return (
<div className="">
{leftDays > 0 ? (
<div className="text-green-500">
{leftDays} / {allDays}
</div>
) : (
<div className="text-red-500"></div>
)}
<div>{new Date(expireAt).toLocaleString().split(" ")[0]} </div>
</div>
);
},
},
{
accessorKey: "workflow",
header: "所属工作流",
cell: ({ row }) => {
const name = row.original.expand.workflow?.name;
const workflowId: string = row.getValue("workflow");
return (
<div className="max-w-[200px] truncate">
<Button
size={"sm"}
variant={"link"}
onClick={() => {
handleWorkflowClick(workflowId);
}}
>
{name}
</Button>
</div>
);
},
},
{
accessorKey: "created",
header: "颁发时间",
cell: ({ row }) => {
const date: string = row.getValue("created");
return new Date(date).toLocaleString();
},
},
{
id: "actions",
cell: ({ row }) => {
return (
<div className="flex items-center space-x-2">
<Button
size="sm"
variant={"link"}
onClick={() => {
handleView(row.original.id);
}}
>
</Button>
</div>
);
},
},
];
const handleWorkflowClick = (id: string) => {
navigate(`/workflow/detail?id=${id}`);
};
const handleView = (id: string) => {
setOpen(true);
const certificate = data.find((item) => item.id === id);
setSelectedCertificate(certificate);
};
return (
<div className="flex flex-col space-y-5">
<div className="text-muted-foreground"></div>
<DataTable columns={columns} onPageChange={fetchData} data={data} pageCount={pageCount} />
<CertificateDetail open={open} onOpenChange={setOpen} certificate={selectedCertificate} />
</div>
);
};
export default Certificate;

View File

@ -66,7 +66,8 @@ const WorkflowDetail = () => {
}, [workflow]);
const handleBackClick = () => {
navigate("/workflow");
// 返回上一步
navigate(-1);
};
const handleEnableChange = () => {

View File

@ -0,0 +1,28 @@
import { Certificate } from "@/domain/certificate";
import { getPb } from "./api";
type CertificateListReq = {
page?: number;
perPage?: number;
};
export const list = async (req: CertificateListReq) => {
const pb = getPb();
let page = 1;
if (req.page) {
page = req.page;
}
let perPage = 2;
if (req.perPage) {
perPage = req.perPage;
}
const response = pb.collection("certificate").getList<Certificate>(page, perPage, {
sort: "-created",
expand: "workflow",
});
return response;
};

View File

@ -65,4 +65,3 @@ export const logs = async (req: WorkflowLogsReq) => {
return response;
};

View File

@ -15,6 +15,7 @@ import Notify from "./pages/setting/Notify";
import SSLProvider from "./pages/setting/SSLProvider";
import Workflow from "./pages/workflow";
import WorkflowDetail from "./pages/workflow/WorkflowDetail";
import Certificate from "./pages/certificate";
export const router = createHashRouter([
{
@ -45,6 +46,10 @@ export const router = createHashRouter([
path: "/workflow",
element: <Workflow />,
},
{
path: "/certificate",
element: <Certificate />,
},
{
path: "/setting",
element: <SettingLayout />,
@ -84,3 +89,4 @@ export const router = createHashRouter([
element: <WorkflowDetail />,
},
]);