Certificate displaying and monitoring

This commit is contained in:
yoan
2024-11-22 10:59:57 +08:00
parent 09e4b24445
commit 86761bd3a0
19 changed files with 498 additions and 381 deletions

View File

@@ -57,19 +57,23 @@ export default function Dashboard() {
<Home className="h-4 w-4" />
{t("dashboard.page.title")}
</Link>
<Link to="/domains" className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/domains"))}>
<Link to="/workflow" className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/workflow"))}>
<Earth className="h-4 w-4" />
{t("domain.page.title")}
</Link>
<Link
to="/certificate"
className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/certificate"))}
>
<History className="h-4 w-4" />
</Link>
<Link to="/access" className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/access"))}>
<Server className="h-4 w-4" />
{t("access.page.title")}
</Link>
<Link to="/history" className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/history"))}>
<History className="h-4 w-4" />
{t("history.page.title")}
</Link>
</nav>
</div>
@@ -99,12 +103,21 @@ export default function Dashboard() {
{t("dashboard.page.title")}
</Link>
<Link
to="/domains"
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/domains"))}
to="/workflow"
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/workflow"))}
>
<Earth className="h-5 w-5" />
{t("domain.page.title")}
</Link>
<Link
to="/certificate"
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/certificate"))}
>
<History className="h-5 w-5" />
</Link>
<Link
to="/access"
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/access"))}
@@ -112,14 +125,6 @@ export default function Dashboard() {
<Server className="h-5 w-5" />
{t("access.page.title")}
</Link>
<Link
to="/history"
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/history"))}
>
<History className="h-5 w-5" />
{t("history.page.title")}
</Link>
</nav>
<div className="">
<Version className="justify-center" />

View File

@@ -1,139 +1,11 @@
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";
import CertificateList from "@/components/certificate/CertificateList";
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} />
<CertificateList withPagination={true} />
</div>
);
};

View File

@@ -1,47 +1,28 @@
import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Ban, CalendarX2, LoaderPinwheel, Smile, SquareSigma } from "lucide-react";
import { CalendarClock, CalendarX2, FolderCheck, SquareSigma, Workflow } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import DeployProgress from "@/components/certimate/DeployProgress";
import DeployState from "@/components/certimate/DeployState";
import { convertZulu2Beijing } from "@/lib/time";
import { Statistic } from "@/domain/domain";
import { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
import { statistics } from "@/repository/domains";
import { list } from "@/repository/deployment";
import { get } from "@/api/statistics";
import CertificateList from "@/components/certificate/CertificateList";
const Dashboard = () => {
const [statistic, setStatistic] = useState<Statistic>();
const [deployments, setDeployments] = useState<Deployment[]>();
const navigate = useNavigate();
const { t } = useTranslation();
useEffect(() => {
const fetchStatistic = async () => {
const data = await statistics();
const data = await get();
setStatistic(data);
};
fetchStatistic();
}, []);
useEffect(() => {
const fetchData = async () => {
const param: DeploymentListReq = {
perPage: 8,
};
const data = await list(param);
setDeployments(data.items);
};
fetchData();
}, []);
return (
<div className="flex flex-col">
<div className="flex justify-between items-center">
@@ -53,12 +34,12 @@ const Dashboard = () => {
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.all")}</div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.total ? (
{statistic?.certificateTotal ? (
<Link to="/domains" className="hover:underline">
{statistic?.total}
{statistic?.certificateTotal}
</Link>
) : (
0
@@ -70,37 +51,37 @@ const Dashboard = () => {
</div>
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
<div className="p-3">
<CalendarClock size={48} strokeWidth={1} className="text-yellow-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.certificateExpireSoon ? (
<Link to="/domains?state=expired" className="hover:underline">
{statistic?.certificateExpireSoon}
</Link>
) : (
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200">{t("dashboard.statistics.unit")}</div>
</div>
</div>
</div>
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
<div className="p-3">
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.near_expired")}</div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.expired ? (
<Link to="/domains?state=expired" className="hover:underline">
{statistic?.expired}
</Link>
) : (
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200">{t("dashboard.statistics.unit")}</div>
</div>
</div>
</div>
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
<div className="p-3">
<LoaderPinwheel size={48} strokeWidth={1} className="text-green-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.enabled")}</div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.enabled ? (
{statistic?.certificateExpired ? (
<Link to="/domains?state=enabled" className="hover:underline">
{statistic?.enabled}
{statistic?.certificateExpired}
</Link>
) : (
0
@@ -113,15 +94,36 @@ const Dashboard = () => {
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
<div className="p-3">
<Ban size={48} strokeWidth={1} className="text-gray-400" />
<Workflow size={48} strokeWidth={1} className="text-emerald-500" />
</div>
<div>
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.disabled")}</div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.disabled ? (
{statistic?.workflowTotal ? (
<Link to="/domains?state=disabled" className="hover:underline">
{statistic?.disabled}
{statistic?.workflowTotal}
</Link>
) : (
0
)}
</div>
<div className="ml-1 text-stone-700 dark:text-stone-200">{t("dashboard.statistics.unit")}</div>
</div>
</div>
</div>
<div className="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
<div className="p-3">
<FolderCheck size={48} strokeWidth={1} className="text-green-400" />
</div>
<div>
<div className="text-muted-foreground font-semibold"></div>
<div className="flex items-baseline">
<div className="text-3xl text-stone-700 dark:text-stone-200">
{statistic?.workflowEnabled ? (
<Link to="/domains?state=disabled" className="hover:underline">
{statistic?.workflowEnabled}
</Link>
) : (
0
@@ -138,132 +140,9 @@ const Dashboard = () => {
</div>
<div>
<div className="text-muted-foreground mt-5 text-sm">{t("dashboard.history")}</div>
<div className="text-muted-foreground mt-5 text-sm"></div>
{deployments?.length == 0 ? (
<>
<Alert className="max-w-[40em] mt-10">
<AlertTitle>{t("common.text.nodata")}</AlertTitle>
<AlertDescription>
<div className="flex items-center mt-5">
<div>
<Smile className="text-yellow-400" size={36} />
</div>
<div className="ml-2"> {t("history.nodata")}</div>
</div>
<div className="mt-2 flex justify-end">
<Button
onClick={() => {
navigate("/edit");
}}
>
{t("domain.add")}
</Button>
</div>
</AlertDescription>
</Alert>
</>
) : (
<>
<div className="hidden sm:flex sm:flex-row text-muted-foreground text-sm border-b dark:border-stone-500 sm:p-2 mt-5">
<div className="w-48">{t("history.props.domain")}</div>
<div className="w-24">{t("history.props.status")}</div>
<div className="w-56">{t("history.props.stage")}</div>
<div className="w-56 sm:ml-2 text-center">{t("history.props.last_execution_time")}</div>
<div className="grow">{t("common.text.operations")}</div>
</div>
{deployments?.map((deployment) => (
<div
key={deployment.id}
className="flex flex-col sm:flex-row text-secondary-foreground border-b dark:border-stone-500 sm:p-2 hover:bg-muted/50 text-sm"
>
<div className="sm:w-48 w-full pt-1 sm:pt-0 flex flex-col items-start">
{deployment.expand.domain?.domain.split(";").map((domain: string) => <div className="pr-3 truncate w-full">{domain}</div>)}
</div>
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
<DeployState deployment={deployment} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center">
<DeployProgress phase={deployment.phase} phaseSuccess={deployment.phaseSuccess} />
</div>
<div className="sm:w-56 w-full pt-1 sm:pt-0 flex items-center sm:justify-center">{convertZulu2Beijing(deployment.deployedAt)}</div>
<div className="flex items-center grow justify-start pt-1 sm:pt-0 sm:ml-2">
<Sheet>
<SheetTrigger asChild>
<Button variant={"link"} className="p-0">
{t("history.log")}
</Button>
</SheetTrigger>
<SheetContent className="sm:max-w-5xl">
<SheetHeader>
<SheetTitle>
{deployment.expand.domain?.domain}-{deployment.id}
{t("history.log")}
</SheetTitle>
</SheetHeader>
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh] overflow-y-auto">
{deployment.log.check && (
<>
{deployment.log.check.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
{deployment.log.apply && (
<>
{deployment.log.apply.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.info &&
item.info.map((info: string) => {
return <div className="mt-1 text-green-600">{info}</div>;
})}
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
{deployment.log.deploy && (
<>
{deployment.log.deploy.map((item: Log) => {
return (
<div className="flex flex-col mt-2">
<div className="flex">
<div>[{item.time}]</div>
<div className="ml-2">{item.message}</div>
</div>
{item.error && <div className="mt-1 text-red-600">{item.error}</div>}
</div>
);
})}
</>
)}
</div>
</SheetContent>
</Sheet>
</div>
</div>
))}
</>
)}
<CertificateList />
</div>
</div>
);

View File

@@ -1,3 +1,4 @@
import { run } from "@/api/workflow";
import Show from "@/components/Show";
import { Button } from "@/components/ui/button";
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
@@ -11,6 +12,7 @@ import WorkflowLog from "@/components/workflow/WorkflowLog";
import WorkflowProvider from "@/components/workflow/WorkflowProvider";
import { allNodesValidated, WorkflowNode } from "@/domain/workflow";
import { getErrMessage } from "@/lib/error";
import { cn } from "@/lib/utils";
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
import { ArrowLeft } from "lucide-react";
@@ -37,6 +39,8 @@ const WorkflowDetail = () => {
const [tab, setTab] = useState("workflow");
const [running, setRunning] = useState(false);
useEffect(() => {
console.log(id);
init(id ?? "");
@@ -101,6 +105,28 @@ const WorkflowDetail = () => {
return "border-transparent hover:text-primary hover:border-b-primary";
};
const handleRunClick = async () => {
if (running) {
return;
}
setRunning(true);
try {
await run(workflow.id as string);
toast({
title: "执行成功",
description: "工作流已成功执行",
variant: "default",
});
} catch (e) {
toast({
title: "执行失败",
description: getErrMessage(e),
variant: "destructive",
});
}
setRunning(false);
};
return (
<>
<WorkflowProvider>
@@ -140,7 +166,14 @@ const WorkflowDetail = () => {
<div className="px-5 flex items-center space-x-3">
<Show when={!!workflow.enabled}>
<Show when={!!workflow.hasDraft} fallback={<Button variant={"secondary"}></Button>}>
<Show
when={!!workflow.hasDraft}
fallback={
<Button variant={"secondary"} onClick={handleRunClick}>
{running ? "执行中" : "立即执行"}
</Button>
}
>
<Button variant={"secondary"} onClick={handleWorkflowSaveClick}>
</Button>

View File

@@ -200,7 +200,7 @@ const Workflow = () => {
</div>
<div>
<DataTable columns={columns} data={data} onPageChange={fetchData} pageCount={pageCount} onRowClick={handleRowClick} />
<DataTable columns={columns} data={data} onPageChange={fetchData} pageCount={pageCount} onRowClick={handleRowClick} withPagination />
</div>
<CustomAlertDialog