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

@ -0,0 +1,11 @@
package domain
type Statistics struct {
CertificateTotal int `json:"certificateTotal"`
CertificateExpireSoon int `json:"certificateExpireSoon"`
CertificateExpired int `json:"certificateExpired"`
WorkflowTotal int `json:"workflowTotal"`
WorkflowEnabled int `json:"workflowEnabled"`
WorkflowDisabled int `json:"workflowDisabled"`
}

View File

@ -19,8 +19,8 @@ const (
func PushExpireMsg() {
// 查询即将过期的证书
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
records, err := app.GetApp().Dao().FindRecordsByFilter("certificate", "expireAt<{:time}&&certUrl!=''", "-created", 500, 0,
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 20)})
if err != nil {
app.GetApp().Logger().Error("find expired domains by filter", "error", err)
return
@ -76,7 +76,7 @@ func buildMsg(records []*models.Record) *notifyMessage {
domains := make([]string, count)
for i, record := range records {
domains[i] = record.GetString("domain")
domains[i] = record.GetString("san")
}
countStr := strconv.Itoa(count)

View File

@ -0,0 +1,63 @@
package repository
import (
"context"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/app"
)
type StatisticsRepository struct{}
func NewStatisticsRepository() *StatisticsRepository {
return &StatisticsRepository{}
}
type totalResp struct {
Total int `json:"total" db:"total"`
}
func (r *StatisticsRepository) Get(ctx context.Context) (*domain.Statistics, error) {
rs := &domain.Statistics{}
// 所有证书
certTotal := totalResp{}
if err := app.GetApp().Dao().DB().NewQuery("select count(*) as total from certificate").One(&certTotal); err != nil {
return nil, err
}
rs.CertificateTotal = certTotal.Total
// 即将过期证书
certExpireSoonTotal := totalResp{}
if err := app.GetApp().Dao().DB().
NewQuery("select count(*) as total from certificate where expireAt > datetime('now') and expireAt < datetime('now', '+20 days')").
One(&certExpireSoonTotal); err != nil {
return nil, err
}
rs.CertificateExpireSoon = certExpireSoonTotal.Total
// 已过期证书
certExpiredTotal := totalResp{}
if err := app.GetApp().Dao().DB().
NewQuery("select count(*) as total from certificate where expireAt < datetime('now')").
One(&certExpiredTotal); err != nil {
return nil, err
}
rs.CertificateExpired = certExpiredTotal.Total
// 所有工作流
workflowTotal := totalResp{}
if err := app.GetApp().Dao().DB().NewQuery("select count(*) as total from workflow").One(&workflowTotal); err != nil {
return nil, err
}
rs.WorkflowTotal = workflowTotal.Total
// 已启用工作流
workflowEnabledTotal := totalResp{}
if err := app.GetApp().Dao().DB().NewQuery("select count(*) as total from workflow where enabled is TRUE").One(&workflowEnabledTotal); err != nil {
return nil, err
}
rs.WorkflowEnabled = workflowEnabledTotal.Total
rs.WorkflowDisabled = workflowTotal.Total - workflowEnabledTotal.Total
return rs, nil
}

View File

@ -0,0 +1,35 @@
package rest
import (
"context"
"github.com/labstack/echo/v5"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/utils/resp"
)
type StatisticsService interface {
Get(ctx context.Context) (*domain.Statistics, error)
}
type statisticsHandler struct {
service StatisticsService
}
func NewStatisticsHandler(route *echo.Group, service StatisticsService) {
handler := &statisticsHandler{
service: service,
}
group := route.Group("/statistics")
group.GET("/get", handler.get)
}
func (handler *statisticsHandler) get(c echo.Context) error {
if statistics, err := handler.service.Get(c.Request().Context()); err != nil {
return resp.Err(c, err)
} else {
return resp.Succ(c, statistics)
}
}

View File

@ -4,6 +4,7 @@ import (
"github.com/usual2970/certimate/internal/notify"
"github.com/usual2970/certimate/internal/repository"
"github.com/usual2970/certimate/internal/rest"
"github.com/usual2970/certimate/internal/statistics"
"github.com/usual2970/certimate/internal/workflow"
"github.com/labstack/echo/v5"
@ -19,7 +20,12 @@ func Register(e *echo.Echo) {
workflowRepo := repository.NewWorkflowRepository()
workflowSvc := workflow.NewWorkflowService(workflowRepo)
statisticsRepo := repository.NewStatisticsRepository()
statisticsSvc := statistics.NewStatisticsService(statisticsRepo)
rest.NewWorkflowHandler(group, workflowSvc)
rest.NewNotifyHandler(group, notifySvc)
rest.NewStatisticsHandler(group, statisticsSvc)
}

View File

@ -0,0 +1,25 @@
package statistics
import (
"context"
"github.com/usual2970/certimate/internal/domain"
)
type StatisticsRepository interface {
Get(ctx context.Context) (*domain.Statistics, error)
}
type StatisticsService struct {
repo StatisticsRepository
}
func NewStatisticsService(repo StatisticsRepository) *StatisticsService {
return &StatisticsService{
repo: repo,
}
}
func (s *StatisticsService) Get(ctx context.Context) (*domain.Statistics, error) {
return s.repo.Get(ctx)
}

15
ui/src/api/statistics.ts Normal file
View File

@ -0,0 +1,15 @@
import { getPb } from "@/repository/api";
export const get = async () => {
const pb = getPb();
const resp = await pb.send("/api/statistics/get", {
method: "GET",
});
if (resp.code != 0) {
throw new Error(resp.msg);
}
return resp.data;
};

21
ui/src/api/workflow.ts Normal file
View File

@ -0,0 +1,21 @@
import { getPb } from "@/repository/api";
export const run = async (id: string) => {
const pb = getPb();
const resp = await pb.send("/api/workflow/run", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: {
id,
},
});
if (resp.code != 0) {
throw new Error(resp.msg);
}
return resp;
};

View File

@ -0,0 +1,162 @@
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";
type CertificateListProps = {
withPagination?: boolean;
};
const CertificateList = ({ withPagination }: CertificateListProps) => {
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 (
<>
<DataTable
columns={columns}
onPageChange={fetchData}
data={data}
pageCount={pageCount}
withPagination={withPagination}
fallback={
<div className="flex flex-col">
<div className="text-muted-foreground">😀</div>
<Button
size={"sm"}
onClick={() => {
navigate("/workflow/detail");
}}
>
</Button>
</div>
}
/>
<CertificateDetail open={open} onOpenChange={setOpen} certificate={selectedCertificate} />
</>
);
};
export default CertificateList;

View File

@ -3,6 +3,7 @@ import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, Paginati
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Button } from "../ui/button";
import { useEffect, useState } from "react";
import Show from "../Show";
interface DataTableProps<TData extends { id: string }, TValue> {
columns: ColumnDef<TData, TValue>[];
@ -10,9 +11,19 @@ interface DataTableProps<TData extends { id: string }, TValue> {
pageCount: number;
onPageChange?: (pageIndex: number, pageSize?: number) => Promise<void>;
onRowClick?: (id: string) => void;
withPagination?: boolean;
fallback?: React.ReactNode;
}
export function DataTable<TData extends { id: string }, TValue>({ columns, data, onPageChange, pageCount, onRowClick }: DataTableProps<TData, TValue>) {
export function DataTable<TData extends { id: string }, TValue>({
columns,
data,
onPageChange,
pageCount,
onRowClick,
withPagination,
fallback,
}: DataTableProps<TData, TValue>) {
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 10,
@ -77,13 +88,14 @@ export function DataTable<TData extends { id: string }, TValue>({ columns, data,
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
{fallback ? fallback : "暂无数据"}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<Show when={!!withPagination}>
<div className="flex items-center justify-end mt-5">
<div className="flex items-center space-x-2 dark:text-stone-200">
{table.getCanPreviousPage() && (
@ -99,6 +111,7 @@ export function DataTable<TData extends { id: string }, TValue>({ columns, data,
)}
</div>
</div>
</Show>
</div>
);
}

View File

@ -57,10 +57,13 @@ export type ApplyConfig = {
};
export type Statistic = {
total: number;
expired: number;
enabled: number;
disabled: number;
certificateTotal: number;
certificateExpired: number;
certificateExpireSoon: number;
workflowTotal: number;
workflowEnabled: number;
workflowDisabled: number;
};
export type DeployTarget = {

View File

@ -84,4 +84,3 @@ export function getTimeAfter(days: number): string {
return formattedDate;
}

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

View File

@ -1,5 +1,5 @@
import { getTimeAfter } from "@/lib/time";
import { Domain, Statistic } from "@/domain/domain";
import { Domain } from "@/domain/domain";
import { getPb } from "./api";
type DomainListReq = {
@ -42,30 +42,6 @@ export const list = async (req: DomainListReq) => {
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

@ -89,4 +89,3 @@ export const router = createHashRouter([
element: <WorkflowDetail />,
},
]);