mirror of https://github.com/usual2970/certimate
Add dashboard
parent
0d5d356a0d
commit
c2d3ed9ff1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -5,8 +5,8 @@
|
|||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Certimate - Your Trusted SSL Automation Partner</title>
|
||||
<script type="module" crossorigin src="/assets/index-BYdgWpkJ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-BPSHHpDP.css">
|
||||
<script type="module" crossorigin src="/assets/index-BFHx9JvV.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Kh_0Jotc.css">
|
||||
</head>
|
||||
<body class="bg-background">
|
||||
<div id="root"></div>
|
||||
|
|
|
@ -23,6 +23,13 @@ export type Domain = {
|
|||
};
|
||||
};
|
||||
|
||||
export type Statistic = {
|
||||
total: number;
|
||||
expired: number;
|
||||
enabled: number;
|
||||
disabled: number;
|
||||
};
|
||||
|
||||
export const getLastDeployment = (domain: Domain): Deployment | undefined => {
|
||||
return domain.expand?.lastDeployment;
|
||||
};
|
||||
|
|
|
@ -20,3 +20,49 @@ export const getDate = (zuluTime: string) => {
|
|||
const time = convertZulu2Beijing(zuluTime);
|
||||
return time.split(" ")[0];
|
||||
};
|
||||
|
||||
export function getTimeBefore(days: number): string {
|
||||
// 获取当前时间
|
||||
const currentDate = new Date();
|
||||
|
||||
// 减去指定的天数
|
||||
currentDate.setUTCDate(currentDate.getUTCDate() - days);
|
||||
|
||||
// 格式化日期为 yyyy-mm-dd
|
||||
const year = currentDate.getUTCFullYear();
|
||||
const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始
|
||||
const day = String(currentDate.getUTCDate()).padStart(2, "0");
|
||||
|
||||
// 格式化时间为 hh:ii:ss
|
||||
const hours = String(currentDate.getUTCHours()).padStart(2, "0");
|
||||
const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0");
|
||||
const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0");
|
||||
|
||||
// 组合成 yyyy-mm-dd hh:ii:ss 格式
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
||||
export function getTimeAfter(days: number): string {
|
||||
// 获取当前时间
|
||||
const currentDate = new Date();
|
||||
|
||||
// 加上指定的天数
|
||||
currentDate.setUTCDate(currentDate.getUTCDate() + days);
|
||||
|
||||
// 格式化日期为 yyyy-mm-dd
|
||||
const year = currentDate.getUTCFullYear();
|
||||
const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始
|
||||
const day = String(currentDate.getUTCDate()).padStart(2, "0");
|
||||
|
||||
// 格式化时间为 hh:ii:ss
|
||||
const hours = String(currentDate.getUTCHours()).padStart(2, "0");
|
||||
const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0");
|
||||
const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0");
|
||||
|
||||
// 组合成 yyyy-mm-dd hh:ii:ss 格式
|
||||
const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
|
||||
return formattedDate;
|
||||
}
|
||||
|
|
|
@ -5,7 +5,15 @@ import {
|
|||
useLocation,
|
||||
useNavigate,
|
||||
} from "react-router-dom";
|
||||
import { CircleUser, Earth, History, Menu, Server } from "lucide-react";
|
||||
import {
|
||||
BookOpen,
|
||||
CircleUser,
|
||||
Earth,
|
||||
History,
|
||||
Home,
|
||||
Menu,
|
||||
Server,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
|
@ -67,6 +75,16 @@ export default function Dashboard() {
|
|||
"flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary",
|
||||
getClass("/")
|
||||
)}
|
||||
>
|
||||
<Home className="h-4 w-4" />
|
||||
控制面板
|
||||
</Link>
|
||||
<Link
|
||||
to="/domains"
|
||||
className={cn(
|
||||
"flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary",
|
||||
getClass("/domains")
|
||||
)}
|
||||
>
|
||||
<Earth className="h-4 w-4" />
|
||||
域名列表
|
||||
|
@ -125,6 +143,16 @@ export default function Dashboard() {
|
|||
"mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground",
|
||||
getClass("/")
|
||||
)}
|
||||
>
|
||||
<Home className="h-5 w-5" />
|
||||
控制面板
|
||||
</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")
|
||||
)}
|
||||
>
|
||||
<Earth className="h-5 w-5" />
|
||||
域名列表
|
||||
|
@ -186,15 +214,20 @@ export default function Dashboard() {
|
|||
<div className="fixed right-0 bottom-0 w-full flex justify-between p-5">
|
||||
<div className=""></div>
|
||||
<div className="text-muted-foreground text-sm hover:text-stone-900 dark:hover:text-stone-200 flex">
|
||||
<a href="https://docs.certimate.me" target="_blank">
|
||||
文档
|
||||
<a
|
||||
href="https://docs.certimate.me"
|
||||
target="_blank"
|
||||
className="flex items-center"
|
||||
>
|
||||
<BookOpen size={16} />
|
||||
<div className="ml-1">文档</div>
|
||||
</a>
|
||||
<Separator orientation="vertical" className="mx-2" />
|
||||
<a
|
||||
href="https://github.com/usual2970/certimate/releases"
|
||||
target="_blank"
|
||||
>
|
||||
Certimate v0.0.15
|
||||
Certimate v0.0.16
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,317 @@
|
|||
import DeployProgress from "@/components/certimate/DeployProgress";
|
||||
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 { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
|
||||
import { Statistic } from "@/domain/domain";
|
||||
import { convertZulu2Beijing } from "@/lib/time";
|
||||
import { list } from "@/repository/deployment";
|
||||
import { statistics } from "@/repository/domains";
|
||||
|
||||
import {
|
||||
Ban,
|
||||
CalendarX2,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
LoaderPinwheel,
|
||||
Smile,
|
||||
SquareSigma,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
|
||||
const Dashboard = () => {
|
||||
const [statistic, setStatistic] = useState<Statistic>();
|
||||
const [deployments, setDeployments] = useState<Deployment[]>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchStatistic = async () => {
|
||||
const data = await statistics();
|
||||
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">
|
||||
<div className="text-muted-foreground">控制面板</div>
|
||||
</div>
|
||||
<div className="flex mt-10 gap-5 flex-col md:flex-row">
|
||||
<div className="w-full md:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
||||
<div className="p-3">
|
||||
<SquareSigma size={48} strokeWidth={1} className="text-blue-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?.total ? (
|
||||
<Link to="/domains" className="hover:underline">
|
||||
{statistic?.total}
|
||||
</Link>
|
||||
) : (
|
||||
0
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1 text-stone-700 dark:text-stone-200">个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
||||
<div className="p-3">
|
||||
<CalendarX2 size={48} strokeWidth={1} className="text-red-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?.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">个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border w-full md: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">启用中</div>
|
||||
<div className="flex items-baseline">
|
||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||
{statistic?.enabled ? (
|
||||
<Link to="/domains?state=enabled" className="hover:underline">
|
||||
{statistic?.enabled}
|
||||
</Link>
|
||||
) : (
|
||||
0
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1 text-stone-700 dark:text-stone-200">个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border w-full md:w-[300px] flex items-center rounded-md p-3 shadow-lg">
|
||||
<div className="p-3">
|
||||
<Ban size={48} strokeWidth={1} className="text-gray-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?.disabled ? (
|
||||
<Link
|
||||
to="/domains?state=disabled"
|
||||
className="hover:underline"
|
||||
>
|
||||
{statistic?.disabled}
|
||||
</Link>
|
||||
) : (
|
||||
0
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-1 text-stone-700 dark:text-stone-200">个</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-muted-foreground mt-5 text-sm">部署历史</div>
|
||||
|
||||
{deployments?.length == 0 ? (
|
||||
<>
|
||||
<Alert className="max-w-[40em] mt-10">
|
||||
<AlertTitle>暂无数据</AlertTitle>
|
||||
<AlertDescription>
|
||||
<div className="flex items-center mt-5">
|
||||
<div>
|
||||
<Smile className="text-yellow-400" size={36} />
|
||||
</div>
|
||||
<div className="ml-2">
|
||||
{" "}
|
||||
你暂未创建任何部署,请先添加域名进行部署吧!
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-2 flex justify-end">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
添加域名
|
||||
</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">域名</div>
|
||||
|
||||
<div className="w-24">状态</div>
|
||||
<div className="w-56">阶段</div>
|
||||
<div className="w-56 sm:ml-2 text-center">最近执行时间</div>
|
||||
|
||||
<div className="grow">操作</div>
|
||||
</div>
|
||||
<div className="sm:hidden flex text-sm text-muted-foreground">
|
||||
部署历史
|
||||
</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 items-center">
|
||||
{deployment.expand.domain?.domain}
|
||||
</div>
|
||||
<div className="sm:w-24 w-full pt-1 sm:pt-0 flex items-center">
|
||||
{deployment.phase === "deploy" && deployment.phaseSuccess ? (
|
||||
<CircleCheck size={16} className="text-green-700" />
|
||||
) : (
|
||||
<CircleX size={16} className="text-red-700" />
|
||||
)}
|
||||
</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">
|
||||
日志
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="sm:max-w-5xl">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{deployment.expand.domain?.domain}-{deployment.id}
|
||||
部署详情
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="bg-gray-950 text-stone-100 p-5 text-sm h-[80dvh]">
|
||||
{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>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dashboard;
|
|
@ -44,6 +44,8 @@ const Home = () => {
|
|||
const query = new URLSearchParams(location.search);
|
||||
const page = query.get("page");
|
||||
|
||||
const state = query.get("state");
|
||||
|
||||
const [totalPage, setTotalPage] = useState(0);
|
||||
|
||||
const handleCreateClick = () => {
|
||||
|
@ -79,13 +81,14 @@ const Home = () => {
|
|||
const data = await list({
|
||||
page: page ? Number(page) : 1,
|
||||
perPage: 10,
|
||||
state: state ? state : "",
|
||||
});
|
||||
|
||||
setDomains(data.items);
|
||||
setTotalPage(data.totalPages);
|
||||
};
|
||||
fetchData();
|
||||
}, [page]);
|
||||
}, [page, state]);
|
||||
|
||||
const handelCheckedChange = async (id: string) => {
|
||||
const checkedDomains = domains.filter((domain) => domain.id === id);
|
||||
|
|
|
@ -4,6 +4,6 @@ console.log(apiDomain);
|
|||
let pb: PocketBase;
|
||||
export const getPb = () => {
|
||||
if (pb) return pb;
|
||||
pb = new PocketBase("/");
|
||||
pb = new PocketBase("http://127.0.0.1:8090");
|
||||
return pb;
|
||||
};
|
||||
|
|
|
@ -1,10 +1,12 @@
|
|||
import { Domain } from "@/domain/domain";
|
||||
import { Domain, Statistic } from "@/domain/domain";
|
||||
import { getPb } from "./api";
|
||||
import { getTimeAfter } from "@/lib/time";
|
||||
|
||||
type DomainListReq = {
|
||||
domain?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
state?: string;
|
||||
};
|
||||
|
||||
export const list = async (req: DomainListReq) => {
|
||||
|
@ -17,16 +19,51 @@ export const list = async (req: DomainListReq) => {
|
|||
if (req.perPage) {
|
||||
perPage = req.perPage;
|
||||
}
|
||||
const response = getPb()
|
||||
.collection("domains")
|
||||
.getList<Domain>(page, perPage, {
|
||||
sort: "-created",
|
||||
expand: "lastDeployment",
|
||||
const pb = getPb();
|
||||
let filter = "";
|
||||
if (req.state === "enabled") {
|
||||
filter = "enabled=true";
|
||||
} else if (req.state === "disabled") {
|
||||
filter = "enabled=false";
|
||||
} else if (req.state === "expired") {
|
||||
filter = pb.filter("expiredAt<{:expiredAt}", {
|
||||
expiredAt: getTimeAfter(15),
|
||||
});
|
||||
}
|
||||
|
||||
const response = pb.collection("domains").getList<Domain>(page, perPage, {
|
||||
sort: "-created",
|
||||
expand: "lastDeployment",
|
||||
filter: filter,
|
||||
});
|
||||
|
||||
return response;
|
||||
};
|
||||
|
||||
export const statistics = async (): Promise<Statistic> => {
|
||||
const pb = getPb();
|
||||
const total = await pb.collection("domains").getList(1, 1, {});
|
||||
const expired = await pb.collection("domains").getList(1, 1, {
|
||||
filter: pb.filter("expiredAt<{:expiredAt}", {
|
||||
expiredAt: getTimeAfter(15),
|
||||
}),
|
||||
});
|
||||
|
||||
const enabled = await pb.collection("domains").getList(1, 1, {
|
||||
filter: "enabled=true",
|
||||
});
|
||||
const disabled = await pb.collection("domains").getList(1, 1, {
|
||||
filter: "enabled=false",
|
||||
});
|
||||
|
||||
return {
|
||||
total: total.totalItems,
|
||||
expired: expired.totalItems,
|
||||
enabled: enabled.totalItems,
|
||||
disabled: disabled.totalItems,
|
||||
};
|
||||
};
|
||||
|
||||
export const get = async (id: string) => {
|
||||
const response = await getPb().collection("domains").getOne<Domain>(id);
|
||||
return response;
|
||||
|
|
|
@ -9,6 +9,7 @@ import Login from "./pages/login/Login";
|
|||
import LoginLayout from "./pages/LoginLayout";
|
||||
import Password from "./pages/setting/Password";
|
||||
import SettingLayout from "./pages/SettingLayout";
|
||||
import Dashboard from "./pages/dashboard/Dashboard";
|
||||
|
||||
export const router = createHashRouter([
|
||||
{
|
||||
|
@ -17,6 +18,10 @@ export const router = createHashRouter([
|
|||
children: [
|
||||
{
|
||||
path: "/",
|
||||
element: <Dashboard />,
|
||||
},
|
||||
{
|
||||
path: "/domains",
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
|
|
Loading…
Reference in New Issue