Add dashboard

This commit is contained in:
yoan 2024-09-07 20:55:36 +08:00
parent 0d5d356a0d
commit c2d3ed9ff1
13 changed files with 747 additions and 269 deletions

284
ui/dist/assets/index-BFHx9JvV.js vendored Normal file

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

1
ui/dist/assets/index-Kh_0Jotc.css vendored Normal file

File diff suppressed because one or more lines are too long

4
ui/dist/index.html vendored
View File

@ -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>

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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>

View File

@ -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;

View File

@ -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);

View File

@ -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;
};

View File

@ -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;

View File

@ -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 />,
},
{