mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
Certificate displaying and monitoring
This commit is contained in:
parent
09e4b24445
commit
86761bd3a0
11
internal/domain/statistics.go
Normal file
11
internal/domain/statistics.go
Normal 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"`
|
||||||
|
}
|
@ -19,8 +19,8 @@ const (
|
|||||||
|
|
||||||
func PushExpireMsg() {
|
func PushExpireMsg() {
|
||||||
// 查询即将过期的证书
|
// 查询即将过期的证书
|
||||||
records, err := app.GetApp().Dao().FindRecordsByFilter("domains", "expiredAt<{:time}&&certUrl!=''", "-created", 500, 0,
|
records, err := app.GetApp().Dao().FindRecordsByFilter("certificate", "expireAt<{:time}&&certUrl!=''", "-created", 500, 0,
|
||||||
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 15)})
|
dbx.Params{"time": xtime.GetTimeAfter(24 * time.Hour * 20)})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.GetApp().Logger().Error("find expired domains by filter", "error", err)
|
app.GetApp().Logger().Error("find expired domains by filter", "error", err)
|
||||||
return
|
return
|
||||||
@ -76,7 +76,7 @@ func buildMsg(records []*models.Record) *notifyMessage {
|
|||||||
domains := make([]string, count)
|
domains := make([]string, count)
|
||||||
|
|
||||||
for i, record := range records {
|
for i, record := range records {
|
||||||
domains[i] = record.GetString("domain")
|
domains[i] = record.GetString("san")
|
||||||
}
|
}
|
||||||
|
|
||||||
countStr := strconv.Itoa(count)
|
countStr := strconv.Itoa(count)
|
||||||
|
63
internal/repository/statistics.go
Normal file
63
internal/repository/statistics.go
Normal 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
|
||||||
|
}
|
35
internal/rest/statistics.go
Normal file
35
internal/rest/statistics.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/usual2970/certimate/internal/notify"
|
"github.com/usual2970/certimate/internal/notify"
|
||||||
"github.com/usual2970/certimate/internal/repository"
|
"github.com/usual2970/certimate/internal/repository"
|
||||||
"github.com/usual2970/certimate/internal/rest"
|
"github.com/usual2970/certimate/internal/rest"
|
||||||
|
"github.com/usual2970/certimate/internal/statistics"
|
||||||
"github.com/usual2970/certimate/internal/workflow"
|
"github.com/usual2970/certimate/internal/workflow"
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
@ -19,7 +20,12 @@ func Register(e *echo.Echo) {
|
|||||||
workflowRepo := repository.NewWorkflowRepository()
|
workflowRepo := repository.NewWorkflowRepository()
|
||||||
workflowSvc := workflow.NewWorkflowService(workflowRepo)
|
workflowSvc := workflow.NewWorkflowService(workflowRepo)
|
||||||
|
|
||||||
|
statisticsRepo := repository.NewStatisticsRepository()
|
||||||
|
statisticsSvc := statistics.NewStatisticsService(statisticsRepo)
|
||||||
|
|
||||||
rest.NewWorkflowHandler(group, workflowSvc)
|
rest.NewWorkflowHandler(group, workflowSvc)
|
||||||
|
|
||||||
rest.NewNotifyHandler(group, notifySvc)
|
rest.NewNotifyHandler(group, notifySvc)
|
||||||
|
|
||||||
|
rest.NewStatisticsHandler(group, statisticsSvc)
|
||||||
}
|
}
|
||||||
|
25
internal/statistics/service.go
Normal file
25
internal/statistics/service.go
Normal 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
15
ui/src/api/statistics.ts
Normal 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
21
ui/src/api/workflow.ts
Normal 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;
|
||||||
|
};
|
162
ui/src/components/certificate/CertificateList.tsx
Normal file
162
ui/src/components/certificate/CertificateList.tsx
Normal 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;
|
@ -3,6 +3,7 @@ import { ColumnDef, flexRender, getCoreRowModel, getPaginationRowModel, Paginati
|
|||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import Show from "../Show";
|
||||||
|
|
||||||
interface DataTableProps<TData extends { id: string }, TValue> {
|
interface DataTableProps<TData extends { id: string }, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
@ -10,9 +11,19 @@ interface DataTableProps<TData extends { id: string }, TValue> {
|
|||||||
pageCount: number;
|
pageCount: number;
|
||||||
onPageChange?: (pageIndex: number, pageSize?: number) => Promise<void>;
|
onPageChange?: (pageIndex: number, pageSize?: number) => Promise<void>;
|
||||||
onRowClick?: (id: string) => 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>({
|
const [{ pageIndex, pageSize }, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
@ -77,13 +88,14 @@ export function DataTable<TData extends { id: string }, TValue>({ columns, data,
|
|||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
No results.
|
{fallback ? fallback : "暂无数据"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
<Show when={!!withPagination}>
|
||||||
<div className="flex items-center justify-end mt-5">
|
<div className="flex items-center justify-end mt-5">
|
||||||
<div className="flex items-center space-x-2 dark:text-stone-200">
|
<div className="flex items-center space-x-2 dark:text-stone-200">
|
||||||
{table.getCanPreviousPage() && (
|
{table.getCanPreviousPage() && (
|
||||||
@ -99,6 +111,7 @@ export function DataTable<TData extends { id: string }, TValue>({ columns, data,
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,10 +57,13 @@ export type ApplyConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Statistic = {
|
export type Statistic = {
|
||||||
total: number;
|
certificateTotal: number;
|
||||||
expired: number;
|
certificateExpired: number;
|
||||||
enabled: number;
|
certificateExpireSoon: number;
|
||||||
disabled: number;
|
|
||||||
|
workflowTotal: number;
|
||||||
|
workflowEnabled: number;
|
||||||
|
workflowDisabled: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeployTarget = {
|
export type DeployTarget = {
|
||||||
|
@ -84,4 +84,3 @@ export function getTimeAfter(days: number): string {
|
|||||||
|
|
||||||
return formattedDate;
|
return formattedDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,19 +57,23 @@ export default function Dashboard() {
|
|||||||
<Home className="h-4 w-4" />
|
<Home className="h-4 w-4" />
|
||||||
{t("dashboard.page.title")}
|
{t("dashboard.page.title")}
|
||||||
</Link>
|
</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" />
|
<Earth className="h-4 w-4" />
|
||||||
{t("domain.page.title")}
|
工作流列表
|
||||||
</Link>
|
</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"))}>
|
<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" />
|
<Server className="h-4 w-4" />
|
||||||
{t("access.page.title")}
|
{t("access.page.title")}
|
||||||
</Link>
|
</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>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -99,12 +103,21 @@ export default function Dashboard() {
|
|||||||
{t("dashboard.page.title")}
|
{t("dashboard.page.title")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/domains"
|
to="/workflow"
|
||||||
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/domains"))}
|
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" />
|
<Earth className="h-5 w-5" />
|
||||||
{t("domain.page.title")}
|
工作流
|
||||||
</Link>
|
</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
|
<Link
|
||||||
to="/access"
|
to="/access"
|
||||||
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/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" />
|
<Server className="h-5 w-5" />
|
||||||
{t("access.page.title")}
|
{t("access.page.title")}
|
||||||
</Link>
|
</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>
|
</nav>
|
||||||
<div className="">
|
<div className="">
|
||||||
<Version className="justify-center" />
|
<Version className="justify-center" />
|
||||||
|
@ -1,139 +1,11 @@
|
|||||||
import CertificateDetail from "@/components/certificate/CertificateDetail";
|
import CertificateList from "@/components/certificate/CertificateList";
|
||||||
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 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 (
|
return (
|
||||||
<div className="flex flex-col space-y-5">
|
<div className="flex flex-col space-y-5">
|
||||||
<div className="text-muted-foreground">证书</div>
|
<div className="text-muted-foreground">证书</div>
|
||||||
|
|
||||||
<DataTable columns={columns} onPageChange={fetchData} data={data} pageCount={pageCount} />
|
<CertificateList withPagination={true} />
|
||||||
|
|
||||||
<CertificateDetail open={open} onOpenChange={setOpen} certificate={selectedCertificate} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,47 +1,28 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
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 { Statistic } from "@/domain/domain";
|
||||||
import { Deployment, DeploymentListReq, Log } from "@/domain/deployment";
|
|
||||||
import { statistics } from "@/repository/domains";
|
import { get } from "@/api/statistics";
|
||||||
import { list } from "@/repository/deployment";
|
|
||||||
|
import CertificateList from "@/components/certificate/CertificateList";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
const [statistic, setStatistic] = useState<Statistic>();
|
const [statistic, setStatistic] = useState<Statistic>();
|
||||||
const [deployments, setDeployments] = useState<Deployment[]>();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStatistic = async () => {
|
const fetchStatistic = async () => {
|
||||||
const data = await statistics();
|
const data = await get();
|
||||||
setStatistic(data);
|
setStatistic(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchStatistic();
|
fetchStatistic();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchData = async () => {
|
|
||||||
const param: DeploymentListReq = {
|
|
||||||
perPage: 8,
|
|
||||||
};
|
|
||||||
|
|
||||||
const data = await list(param);
|
|
||||||
setDeployments(data.items);
|
|
||||||
};
|
|
||||||
fetchData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
@ -53,12 +34,12 @@ const Dashboard = () => {
|
|||||||
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
|
<SquareSigma size={48} strokeWidth={1} className="text-blue-400" />
|
||||||
</div>
|
</div>
|
||||||
<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="flex items-baseline">
|
||||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||||
{statistic?.total ? (
|
{statistic?.certificateTotal ? (
|
||||||
<Link to="/domains" className="hover:underline">
|
<Link to="/domains" className="hover:underline">
|
||||||
{statistic?.total}
|
{statistic?.certificateTotal}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
0
|
0
|
||||||
@ -70,37 +51,37 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg border">
|
<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">
|
<div className="p-3">
|
||||||
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
|
<CalendarX2 size={48} strokeWidth={1} className="text-red-400" />
|
||||||
</div>
|
</div>
|
||||||
<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="flex items-baseline">
|
||||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||||
{statistic?.expired ? (
|
{statistic?.certificateExpired ? (
|
||||||
<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 ? (
|
|
||||||
<Link to="/domains?state=enabled" className="hover:underline">
|
<Link to="/domains?state=enabled" className="hover:underline">
|
||||||
{statistic?.enabled}
|
{statistic?.certificateExpired}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
0
|
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="border w-full md:w-[250px] 3xl:w-[300px] flex items-center rounded-md p-3 shadow-lg">
|
||||||
<div className="p-3">
|
<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>
|
<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="flex items-baseline">
|
||||||
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
<div className="text-3xl text-stone-700 dark:text-stone-200">
|
||||||
{statistic?.disabled ? (
|
{statistic?.workflowTotal ? (
|
||||||
<Link to="/domains?state=disabled" className="hover:underline">
|
<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>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
0
|
0
|
||||||
@ -138,132 +140,9 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 ? (
|
<CertificateList />
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { run } from "@/api/workflow";
|
||||||
import Show from "@/components/Show";
|
import Show from "@/components/Show";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
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 WorkflowProvider from "@/components/workflow/WorkflowProvider";
|
||||||
import { allNodesValidated, WorkflowNode } from "@/domain/workflow";
|
import { allNodesValidated, WorkflowNode } from "@/domain/workflow";
|
||||||
|
import { getErrMessage } from "@/lib/error";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
|
import { useWorkflowStore, WorkflowState } from "@/providers/workflow";
|
||||||
import { ArrowLeft } from "lucide-react";
|
import { ArrowLeft } from "lucide-react";
|
||||||
@ -37,6 +39,8 @@ const WorkflowDetail = () => {
|
|||||||
|
|
||||||
const [tab, setTab] = useState("workflow");
|
const [tab, setTab] = useState("workflow");
|
||||||
|
|
||||||
|
const [running, setRunning] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log(id);
|
console.log(id);
|
||||||
init(id ?? "");
|
init(id ?? "");
|
||||||
@ -101,6 +105,28 @@ const WorkflowDetail = () => {
|
|||||||
return "border-transparent hover:text-primary hover:border-b-primary";
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<WorkflowProvider>
|
<WorkflowProvider>
|
||||||
@ -140,7 +166,14 @@ const WorkflowDetail = () => {
|
|||||||
|
|
||||||
<div className="px-5 flex items-center space-x-3">
|
<div className="px-5 flex items-center space-x-3">
|
||||||
<Show when={!!workflow.enabled}>
|
<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 variant={"secondary"} onClick={handleWorkflowSaveClick}>
|
||||||
保存变更
|
保存变更
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -200,7 +200,7 @@ const Workflow = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<CustomAlertDialog
|
<CustomAlertDialog
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { getTimeAfter } from "@/lib/time";
|
import { getTimeAfter } from "@/lib/time";
|
||||||
import { Domain, Statistic } from "@/domain/domain";
|
import { Domain } from "@/domain/domain";
|
||||||
import { getPb } from "./api";
|
import { getPb } from "./api";
|
||||||
|
|
||||||
type DomainListReq = {
|
type DomainListReq = {
|
||||||
@ -42,30 +42,6 @@ export const list = async (req: DomainListReq) => {
|
|||||||
return response;
|
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) => {
|
export const get = async (id: string) => {
|
||||||
const response = await getPb().collection("domains").getOne<Domain>(id);
|
const response = await getPb().collection("domains").getOne<Domain>(id);
|
||||||
return response;
|
return response;
|
||||||
|
@ -89,4 +89,3 @@ export const router = createHashRouter([
|
|||||||
element: <WorkflowDetail />,
|
element: <WorkflowDetail />,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user