mirror of
https://github.com/usual2970/certimate.git
synced 2025-06-10 14:39:50 +00:00
certificate display
This commit is contained in:
parent
2d10fa0218
commit
09e4b24445
@ -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 ""
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
63
ui/src/components/certificate/CertificateDetail.tsx
Normal file
63
ui/src/components/certificate/CertificateDetail.tsx
Normal 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;
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
||||
|
21
ui/src/domain/certificate.ts
Normal file
21
ui/src/domain/certificate.ts
Normal 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;
|
||||
};
|
||||
};
|
@ -469,4 +469,3 @@ export const workflowNodeDropdownList: WorkflowwNodeDropdwonItem[] = [
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -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();
|
||||
|
141
ui/src/pages/certificate/index.tsx
Normal file
141
ui/src/pages/certificate/index.tsx
Normal 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;
|
@ -66,7 +66,8 @@ const WorkflowDetail = () => {
|
||||
}, [workflow]);
|
||||
|
||||
const handleBackClick = () => {
|
||||
navigate("/workflow");
|
||||
// 返回上一步
|
||||
navigate(-1);
|
||||
};
|
||||
|
||||
const handleEnableChange = () => {
|
||||
|
28
ui/src/repository/certificate.ts
Normal file
28
ui/src/repository/certificate.ts
Normal 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;
|
||||
};
|
@ -65,4 +65,3 @@ export const logs = async (req: WorkflowLogsReq) => {
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
|
@ -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 />,
|
||||
},
|
||||
]);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user