mirror of
https://github.com/woodchen-ink/certimate.git
synced 2025-07-18 17:31:55 +08:00
improve multi language
This commit is contained in:
parent
47050769fc
commit
37df882ed3
@ -5,6 +5,7 @@ import { Textarea } from "../ui/textarea";
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { Label } from "../ui/label";
|
import { Label } from "../ui/label";
|
||||||
import { CustomFile, saveFiles2ZIP } from "@/lib/file";
|
import { CustomFile, saveFiles2ZIP } from "@/lib/file";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type WorkflowLogDetailProps = {
|
type WorkflowLogDetailProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -12,6 +13,7 @@ type WorkflowLogDetailProps = {
|
|||||||
certificate?: Certificate;
|
certificate?: Certificate;
|
||||||
};
|
};
|
||||||
const CertificateDetail = ({ open, onOpenChange, certificate }: WorkflowLogDetailProps) => {
|
const CertificateDetail = ({ open, onOpenChange, certificate }: WorkflowLogDetailProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const handleDownloadClick = async () => {
|
const handleDownloadClick = async () => {
|
||||||
const zipName = `${certificate?.id}-${certificate?.san}.zip`;
|
const zipName = `${certificate?.id}-${certificate?.san}.zip`;
|
||||||
const files: CustomFile[] = [
|
const files: CustomFile[] = [
|
||||||
@ -30,7 +32,7 @@ const CertificateDetail = ({ open, onOpenChange, certificate }: WorkflowLogDetai
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent className="sm:max-w-2xl">
|
<SheetContent className="sm:max-w-2xl dark:text-stone-200">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle></SheetTitle>
|
<SheetTitle></SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
@ -43,15 +45,15 @@ const CertificateDetail = ({ open, onOpenChange, certificate }: WorkflowLogDetai
|
|||||||
handleDownloadClick();
|
handleDownloadClick();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
下载证书
|
{t("certificate.action.download")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<Label>证书</Label>
|
<Label>{t("certificate.props.certificate")}</Label>
|
||||||
<Textarea value={certificate?.certificate} rows={10} readOnly={true} />
|
<Textarea value={certificate?.certificate} rows={10} readOnly={true} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<Label>密钥</Label>
|
<Label>{t("certificate.props.private.key")}</Label>
|
||||||
<Textarea value={certificate?.privateKey} rows={10} readOnly={true} />
|
<Textarea value={certificate?.privateKey} rows={10} readOnly={true} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,8 @@ import { diffDays, getLeftDays } from "@/lib/time";
|
|||||||
import { list } from "@/repository/certificate";
|
import { list } from "@/repository/certificate";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
type CertificateListProps = {
|
type CertificateListProps = {
|
||||||
withPagination?: boolean;
|
withPagination?: boolean;
|
||||||
@ -18,8 +19,13 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
|||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedCertificate, setSelectedCertificate] = useState<CertificateType>();
|
const [selectedCertificate, setSelectedCertificate] = useState<CertificateType>();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const fetchData = async (page: number, pageSize?: number) => {
|
const fetchData = async (page: number, pageSize?: number) => {
|
||||||
const resp = await list({ page: page, perPage: pageSize });
|
const state = searchParams.get("state");
|
||||||
|
const resp = await list({ page: page, perPage: pageSize, state: state ?? "" });
|
||||||
setData(resp.items);
|
setData(resp.items);
|
||||||
setPageCount(resp.totalPages);
|
setPageCount(resp.totalPages);
|
||||||
};
|
};
|
||||||
@ -29,7 +35,7 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
|||||||
const columns: ColumnDef<CertificateType>[] = [
|
const columns: ColumnDef<CertificateType>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "san",
|
accessorKey: "san",
|
||||||
header: "域名",
|
header: t("certificate.props.domain"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
let san: string = row.getValue("san");
|
let san: string = row.getValue("san");
|
||||||
if (!san) {
|
if (!san) {
|
||||||
@ -51,7 +57,7 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "expireAt",
|
accessorKey: "expireAt",
|
||||||
header: "有效期限",
|
header: t("certificate.props.expiry"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const expireAt: string = row.getValue("expireAt");
|
const expireAt: string = row.getValue("expireAt");
|
||||||
const data = row.original;
|
const data = row.original;
|
||||||
@ -61,20 +67,22 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
|||||||
<div className="">
|
<div className="">
|
||||||
{leftDays > 0 ? (
|
{leftDays > 0 ? (
|
||||||
<div className="text-green-500">
|
<div className="text-green-500">
|
||||||
{leftDays} / {allDays} 天
|
{leftDays} / {allDays} {t("certificate.props.expiry.days")}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-red-500">已到期</div>
|
<div className="text-red-500">{t("certificate.props.expiry.expired")}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div>{new Date(expireAt).toLocaleString().split(" ")[0]} 到期</div>
|
<div>
|
||||||
|
{new Date(expireAt).toLocaleString().split(" ")[0]} {t("certificate.props.expiry.text.expire")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "workflow",
|
accessorKey: "workflow",
|
||||||
header: "所属工作流",
|
header: t("certificate.props.workflow"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const name = row.original.expand.workflow?.name;
|
const name = row.original.expand.workflow?.name;
|
||||||
const workflowId: string = row.getValue("workflow");
|
const workflowId: string = row.getValue("workflow");
|
||||||
@ -95,7 +103,7 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "created",
|
accessorKey: "created",
|
||||||
header: "颁发时间",
|
header: t("certificate.props.created"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const date: string = row.getValue("created");
|
const date: string = row.getValue("created");
|
||||||
return new Date(date).toLocaleString();
|
return new Date(date).toLocaleString();
|
||||||
@ -113,7 +121,7 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
|||||||
handleView(row.original.id);
|
handleView(row.original.id);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
查看证书
|
{t("certificate.action.view")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -140,15 +148,16 @@ const CertificateList = ({ withPagination }: CertificateListProps) => {
|
|||||||
pageCount={pageCount}
|
pageCount={pageCount}
|
||||||
withPagination={withPagination}
|
withPagination={withPagination}
|
||||||
fallback={
|
fallback={
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col items-center">
|
||||||
<div className="text-muted-foreground">暂无证书,添加工作流去生成证书吧😀</div>
|
<div className="text-muted-foreground">{t("certificate.nodata")}</div>
|
||||||
<Button
|
<Button
|
||||||
size={"sm"}
|
size={"sm"}
|
||||||
|
className="w-[120px] mt-3"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/workflow/detail");
|
navigate("/workflow/detail");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
添加工作流
|
{t("workflow.action.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@
|
|||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Show from "../Show";
|
import Show from "../Show";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
interface DataTableProps<TData extends { id: string }, TValue> {
|
interface DataTableProps<TData extends { id: string }, TValue> {
|
||||||
columns: ColumnDef<TData, TValue>[];
|
columns: ColumnDef<TData, TValue>[];
|
||||||
@ -29,6 +30,8 @@ export function DataTable<TData extends { id: string }, TValue>({
|
|||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const pagination = {
|
const pagination = {
|
||||||
pageIndex,
|
pageIndex,
|
||||||
pageSize,
|
pageSize,
|
||||||
@ -88,7 +91,7 @@ export function DataTable<TData extends { id: string }, TValue>({
|
|||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
{fallback ? fallback : "暂无数据"}
|
{fallback ? fallback : t("common.text.nodata")}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
@ -100,13 +103,13 @@ export function DataTable<TData extends { id: string }, TValue>({
|
|||||||
<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() && (
|
||||||
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
|
<Button variant="outline" size="sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>
|
||||||
上一页
|
{t("common.pagination.prev")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{table.getCanNextPage && (
|
{table.getCanNextPage && (
|
||||||
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
<Button variant="outline" size="sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>
|
||||||
下一页
|
{t("common.pagination.next")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,7 +8,7 @@ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "
|
|||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { memo, useEffect, useState } from "react";
|
import { memo, useEffect, useMemo, useState } from "react";
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
|
|
||||||
type WorkflowNameEditDialogProps = {
|
type WorkflowNameEditDialogProps = {
|
||||||
@ -31,9 +31,11 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
|||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const memoWorkflow = useMemo(() => workflow, [workflow]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
form.reset({ name: workflow.name, description: workflow.description });
|
form.reset({ name: workflow.name, description: workflow.description });
|
||||||
}, [workflow]);
|
}, [memoWorkflow]);
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
|||||||
<DialogTrigger>{trigger}</DialogTrigger>
|
<DialogTrigger>{trigger}</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>基础信息</DialogTitle>
|
<DialogTitle className="dark:text-stone-200">{t("workflow.baseinfo.title")}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div>
|
<div>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@ -64,17 +66,17 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
form.handleSubmit(onSubmit)(e);
|
form.handleSubmit(onSubmit)(e);
|
||||||
}}
|
}}
|
||||||
className="space-y-8"
|
className="space-y-8 dark:text-stone-200"
|
||||||
>
|
>
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>名称</FormLabel>
|
<FormLabel>{t("workflow.props.name")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="请输入流程名称"
|
placeholder={t("workflow.props.name.placeholder")}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
defaultValue={workflow.name}
|
defaultValue={workflow.name}
|
||||||
@ -94,10 +96,10 @@ const WorkflowNameBaseInfoDialog = ({ trigger }: WorkflowNameEditDialogProps) =>
|
|||||||
name="description"
|
name="description"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>说明</FormLabel>
|
<FormLabel>{t("workflow.props.description")}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="请输入流程说明"
|
placeholder={t("workflow.props.description.placeholder")}
|
||||||
{...field}
|
{...field}
|
||||||
value={field.value}
|
value={field.value}
|
||||||
defaultValue={workflow.description}
|
defaultValue={workflow.description}
|
||||||
|
@ -6,6 +6,7 @@ import { DataTable } from "./DataTable";
|
|||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, X } from "lucide-react";
|
||||||
import WorkflowLogDetail from "./WorkflowLogDetail";
|
import WorkflowLogDetail from "./WorkflowLogDetail";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const WorkflowLog = () => {
|
const WorkflowLog = () => {
|
||||||
const [data, setData] = useState<WorkflowRunLog[]>([]);
|
const [data, setData] = useState<WorkflowRunLog[]>([]);
|
||||||
@ -14,6 +15,8 @@ const WorkflowLog = () => {
|
|||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const id = searchParams.get("id");
|
const id = searchParams.get("id");
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [selectedLog, setSelectedLog] = useState<WorkflowRunLog>();
|
const [selectedLog, setSelectedLog] = useState<WorkflowRunLog>();
|
||||||
|
|
||||||
@ -26,7 +29,7 @@ const WorkflowLog = () => {
|
|||||||
const columns: ColumnDef<WorkflowRunLog>[] = [
|
const columns: ColumnDef<WorkflowRunLog>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "succeed",
|
accessorKey: "succeed",
|
||||||
header: "状态",
|
header: t("workflow.history.props.state"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const succeed: boolean = row.getValue("succeed");
|
const succeed: boolean = row.getValue("succeed");
|
||||||
if (succeed) {
|
if (succeed) {
|
||||||
@ -35,7 +38,7 @@ const WorkflowLog = () => {
|
|||||||
<div className="text-white bg-green-500 w-8 h-8 rounded-full flex items-center justify-center">
|
<div className="text-white bg-green-500 w-8 h-8 rounded-full flex items-center justify-center">
|
||||||
<Check size={18} />
|
<Check size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-sone-700">通过</div>
|
<div className="text-sone-700">{t("workflow.history.props.state.success")}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -44,7 +47,7 @@ const WorkflowLog = () => {
|
|||||||
<div className="text-white bg-red-500 w-8 h-8 rounded-full flex items-center justify-center">
|
<div className="text-white bg-red-500 w-8 h-8 rounded-full flex items-center justify-center">
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-stone-700">失败</div>
|
<div className="text-stone-700">{t("workflow.history.props.state.failed")}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -52,7 +55,7 @@ const WorkflowLog = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "error",
|
accessorKey: "error",
|
||||||
header: "原因",
|
header: t("workflow.history.props.reason"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
let error: string = row.getValue("error");
|
let error: string = row.getValue("error");
|
||||||
if (!error) {
|
if (!error) {
|
||||||
@ -63,7 +66,7 @@ const WorkflowLog = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "created",
|
accessorKey: "created",
|
||||||
header: "时间",
|
header: t("workflow.history.props.time"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const date: string = row.getValue("created");
|
const date: string = row.getValue("created");
|
||||||
return new Date(date).toLocaleString();
|
return new Date(date).toLocaleString();
|
||||||
@ -79,7 +82,7 @@ const WorkflowLog = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="w-full md:w-[960px]">
|
<div className="w-full md:w-[960px]">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground mb-5">日志</div>
|
<div className="text-muted-foreground mb-5">{t("workflow.history.page.title")}</div>
|
||||||
<DataTable columns={columns} data={data} onPageChange={fetchData} pageCount={pageCount} onRowClick={handleRowClick} />
|
<DataTable columns={columns} data={data} onPageChange={fetchData} pageCount={pageCount} onRowClick={handleRowClick} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import { WorkflowOutput, WorkflowRunLog, WorkflowRunLogItem } from "@/domain/wor
|
|||||||
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "../ui/sheet";
|
import { Sheet, SheetContent, SheetHeader, SheetTitle } from "../ui/sheet";
|
||||||
import { Check, X } from "lucide-react";
|
import { Check, X } from "lucide-react";
|
||||||
import { ScrollArea } from "../ui/scroll-area";
|
import { ScrollArea } from "../ui/scroll-area";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type WorkflowLogDetailProps = {
|
type WorkflowLogDetailProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -9,11 +10,12 @@ type WorkflowLogDetailProps = {
|
|||||||
log?: WorkflowRunLog;
|
log?: WorkflowRunLog;
|
||||||
};
|
};
|
||||||
const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps) => {
|
const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||||
<SheetContent className="sm:max-w-5xl">
|
<SheetContent className="sm:max-w-5xl">
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>日志</SheetTitle>
|
<SheetTitle>{t("workflow.history.page.title")}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
|
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@ -23,7 +25,7 @@ const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps)
|
|||||||
<div className="w-8 h-8 bg-green-500 flex items-center justify-center rounded-full text-white">
|
<div className="w-8 h-8 bg-green-500 flex items-center justify-center rounded-full text-white">
|
||||||
<Check size={18} />
|
<Check size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-stone-700">成功</div>
|
<div className="text-stone-700">{t("workflow.history.props.state.success")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-muted-foreground">{new Date(log.created).toLocaleString()}</div>
|
<div className="text-muted-foreground">{new Date(log.created).toLocaleString()}</div>
|
||||||
@ -34,7 +36,7 @@ const WorkflowLogDetail = ({ open, onOpenChange, log }: WorkflowLogDetailProps)
|
|||||||
<div className="w-8 h-8 bg-red-500 flex items-center justify-center rounded-full text-white">
|
<div className="w-8 h-8 bg-red-500 flex items-center justify-center rounded-full text-white">
|
||||||
<X size={18} />
|
<X size={18} />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-stone-700">失败</div>
|
<div className="text-stone-700">{t("workflow.history.props.state.failed")}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-red-500 max-w-[400px] truncate">{log?.error}</div>
|
<div className="text-red-500 max-w-[400px] truncate">{log?.error}</div>
|
||||||
|
@ -148,7 +148,7 @@ export const initWorkflow = (): Workflow => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: "",
|
id: "",
|
||||||
name: i18n.t("workflow.default.name"),
|
name: i18n.t("workflow.props.name.default"),
|
||||||
type: "auto",
|
type: "auto",
|
||||||
crontab: "0 0 * * *",
|
crontab: "0 0 * * *",
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -6,6 +6,7 @@ import nlsDomain from "./nls.domain.json";
|
|||||||
import nlsAccess from "./nls.access.json";
|
import nlsAccess from "./nls.access.json";
|
||||||
import nlsHistory from "./nls.history.json";
|
import nlsHistory from "./nls.history.json";
|
||||||
import nlsWorkflow from "./nls.workflow.json";
|
import nlsWorkflow from "./nls.workflow.json";
|
||||||
|
import nlsCertificate from "./nls.certificate.json";
|
||||||
|
|
||||||
export default Object.freeze({
|
export default Object.freeze({
|
||||||
...nlsCommon,
|
...nlsCommon,
|
||||||
@ -16,4 +17,5 @@ export default Object.freeze({
|
|||||||
...nlsAccess,
|
...nlsAccess,
|
||||||
...nlsHistory,
|
...nlsHistory,
|
||||||
...nlsWorkflow,
|
...nlsWorkflow,
|
||||||
|
...nlsCertificate,
|
||||||
});
|
});
|
||||||
|
19
ui/src/i18n/locales/en/nls.certificate.json
Normal file
19
ui/src/i18n/locales/en/nls.certificate.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"certificate.page.title": "Certificates",
|
||||||
|
|
||||||
|
"certificate.nodata": "No certificates yet, add a workflow to generate certificates!😀",
|
||||||
|
|
||||||
|
"certificate.props.domain": "Name",
|
||||||
|
"certificate.props.expiry": "Expiry",
|
||||||
|
"certificate.props.expiry.days": "Days",
|
||||||
|
"certificate.props.expiry.expired": "Expired",
|
||||||
|
"certificate.props.expiry.text.expire": "Expire",
|
||||||
|
"certificate.props.workflow": "Workflow",
|
||||||
|
"certificate.props.created": "Created",
|
||||||
|
"certificate.props.certificate": "Certificate",
|
||||||
|
"certificate.props.private.key": "Private Key",
|
||||||
|
|
||||||
|
"certificate.action.view": "View Certificate",
|
||||||
|
"certificate.action.download": "Download Certificate"
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"dashboard.page.title": "Dashboard",
|
"dashboard.page.title": "Dashboard",
|
||||||
|
|
||||||
"dashboard.statistics.all": "All",
|
"dashboard.statistics.all.certificate": "All Certificates",
|
||||||
"dashboard.statistics.near_expired": "About to Expire",
|
"dashboard.statistics.near_expired.certificate": "Certificates Near Expiry",
|
||||||
"dashboard.statistics.enabled": "Enabled",
|
"dashboard.statistics.expired.certificate": "Expired Certificates",
|
||||||
"dashboard.statistics.disabled": "Not Enabled",
|
|
||||||
|
"dashboard.statistics.all.workflow": "All Workflows",
|
||||||
|
"dashboard.statistics.enabled.workflow": "Enabled Workflows",
|
||||||
|
|
||||||
"dashboard.statistics.unit": "",
|
"dashboard.statistics.unit": "",
|
||||||
|
|
||||||
"dashboard.history": "Recently Deployment History"
|
"dashboard.certificate": "Latest Certificate"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,42 @@
|
|||||||
{
|
{
|
||||||
|
"workflow.page.title": "Workflows",
|
||||||
|
"workflow.detail.title": "Workflow",
|
||||||
|
"workflow.detail.history": "History",
|
||||||
|
|
||||||
|
"workflow.detail.action.save": "Save updates",
|
||||||
|
"workflow.detail.action.save.failed": "Save failed",
|
||||||
|
"workflow.detail.action.save.failed.uncompleted": "Save failed, please complete all node settings",
|
||||||
|
"workflow.detail.action.run": "Run",
|
||||||
|
"workflow.detail.action.run.failed": "Run failed",
|
||||||
|
"workflow.detail.action.run.success": "Run success",
|
||||||
|
"workflow.detail.action.running": "Running",
|
||||||
|
|
||||||
|
"workflow.baseinfo.title": "Basic Information",
|
||||||
|
|
||||||
|
"workflow.props.name": "Name",
|
||||||
|
"workflow.props.name.placeholder": "Please enter name",
|
||||||
|
"workflow.props.name.default": "Unnamed",
|
||||||
|
"workflow.props.description": "Description",
|
||||||
|
"workflow.props.description.placeholder": "Please enter description",
|
||||||
|
"workflow.props.executionMethod": "Execution Method",
|
||||||
|
"workflow.props.enabled": "Enabled",
|
||||||
|
"workflow.props.created": "Created",
|
||||||
|
"workflow.props.updated": "Updated",
|
||||||
|
|
||||||
|
"workflow.action": "Action",
|
||||||
|
"workflow.action.edit": "Edit",
|
||||||
|
"workflow.action.create": "Create Workflow",
|
||||||
|
|
||||||
|
"workflow.action.delete.alert.title": "Delete Workflow",
|
||||||
|
"workflow.action.delete.alert.description": "Are you sure you want to delete this workflow?",
|
||||||
|
|
||||||
|
"workflow.history.page.title": "Logs",
|
||||||
|
"workflow.history.props.state": "State",
|
||||||
|
"workflow.history.props.state.success": "Success",
|
||||||
|
"workflow.history.props.state.failed": "Failed",
|
||||||
|
"workflow.history.props.reason": "Reason",
|
||||||
|
"workflow.history.props.time": "Time",
|
||||||
|
|
||||||
"workflow.common.certificate.label": "Certificate",
|
"workflow.common.certificate.label": "Certificate",
|
||||||
"workflow.common.certificate.placeholder": "Please select certificate source",
|
"workflow.common.certificate.placeholder": "Please select certificate source",
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ import nlsDomain from "./nls.domain.json";
|
|||||||
import nlsAccess from "./nls.access.json";
|
import nlsAccess from "./nls.access.json";
|
||||||
import nlsHistory from "./nls.history.json";
|
import nlsHistory from "./nls.history.json";
|
||||||
import nlsWorkflow from "./nls.workflow.json";
|
import nlsWorkflow from "./nls.workflow.json";
|
||||||
|
import nlsCertificate from "./nls.certificate.json";
|
||||||
|
|
||||||
export default Object.freeze({
|
export default Object.freeze({
|
||||||
...nlsCommon,
|
...nlsCommon,
|
||||||
@ -16,4 +17,5 @@ export default Object.freeze({
|
|||||||
...nlsAccess,
|
...nlsAccess,
|
||||||
...nlsHistory,
|
...nlsHistory,
|
||||||
...nlsWorkflow,
|
...nlsWorkflow,
|
||||||
|
...nlsCertificate,
|
||||||
});
|
});
|
||||||
|
19
ui/src/i18n/locales/zh/nls.certificate.json
Normal file
19
ui/src/i18n/locales/zh/nls.certificate.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"certificate.page.title": "证书",
|
||||||
|
|
||||||
|
"certificate.nodata": "暂无证书,添加工作流去生成证书吧😀",
|
||||||
|
|
||||||
|
"certificate.props.domain": "名称",
|
||||||
|
"certificate.props.expiry": "有效期限",
|
||||||
|
"certificate.props.expiry.days": "天",
|
||||||
|
"certificate.props.expiry.expired": "已到期",
|
||||||
|
"certificate.props.expiry.text.expire": "到期",
|
||||||
|
"certificate.props.workflow": "所属工作流",
|
||||||
|
"certificate.props.created": "颁发时间",
|
||||||
|
"certificate.props.certificate": "证书",
|
||||||
|
"certificate.props.private.key": "私钥",
|
||||||
|
|
||||||
|
"certificate.action.view": "查看证书",
|
||||||
|
"certificate.action.download": "下载证书"
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,15 @@
|
|||||||
{
|
{
|
||||||
"dashboard.page.title": "仪表盘",
|
"dashboard.page.title": "仪表盘",
|
||||||
|
|
||||||
"dashboard.statistics.all": "所有",
|
"dashboard.statistics.all.certificate": "所有证书",
|
||||||
"dashboard.statistics.near_expired": "即将过期",
|
"dashboard.statistics.near_expired.certificate": "即将过期证书",
|
||||||
"dashboard.statistics.enabled": "启用中",
|
"dashboard.statistics.expired.certificate": "已过期证书",
|
||||||
"dashboard.statistics.disabled": "未启用",
|
|
||||||
|
"dashboard.statistics.all.workflow": "所有工作流",
|
||||||
|
"dashboard.statistics.enabled.workflow": "已启用工作流",
|
||||||
|
|
||||||
"dashboard.statistics.unit": "个",
|
"dashboard.statistics.unit": "个",
|
||||||
|
|
||||||
"dashboard.history": "最近部署"
|
"dashboard.certificate": "最新证书"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,42 @@
|
|||||||
{
|
{
|
||||||
|
"workflow.page.title": "工作流",
|
||||||
|
"workflow.detail.title": "流程",
|
||||||
|
"workflow.detail.history": "历史",
|
||||||
|
|
||||||
|
"workflow.detail.action.save": "保存变更",
|
||||||
|
"workflow.detail.action.save.failed": "保存失败",
|
||||||
|
"workflow.detail.action.save.failed.uncompleted": "保存失败,请完成所有节点设置",
|
||||||
|
"workflow.detail.action.run": "立即执行",
|
||||||
|
"workflow.detail.action.run.failed": "执行失败",
|
||||||
|
"workflow.detail.action.run.success": "执行成功",
|
||||||
|
"workflow.detail.action.running": "正在执行",
|
||||||
|
|
||||||
|
"workflow.baseinfo.title": "基本信息",
|
||||||
|
|
||||||
|
"workflow.props.name": "名称",
|
||||||
|
"workflow.props.name.placeholder": "请输入名称",
|
||||||
|
"workflow.props.name.default": "未命名工作流",
|
||||||
|
"workflow.props.description": "描述",
|
||||||
|
"workflow.props.description.placeholder": "请输入描述",
|
||||||
|
"workflow.props.executionMethod": "执行方式",
|
||||||
|
"workflow.props.enabled": "是否启用",
|
||||||
|
"workflow.props.created": "创建时间",
|
||||||
|
"workflow.props.updated": "更新时间",
|
||||||
|
|
||||||
|
"workflow.action": "操作",
|
||||||
|
"workflow.action.edit": "编辑",
|
||||||
|
"workflow.action.create": "创建工作流",
|
||||||
|
|
||||||
|
"workflow.action.delete.alert.title": "删除工作流",
|
||||||
|
"workflow.action.delete.alert.description": "确定要删除此工作流吗?",
|
||||||
|
|
||||||
|
"workflow.history.page.title": "日志",
|
||||||
|
"workflow.history.props.state": "状态",
|
||||||
|
"workflow.history.props.state.success": "通过",
|
||||||
|
"workflow.history.props.state.failed": "失败",
|
||||||
|
"workflow.history.props.reason": "原因",
|
||||||
|
"workflow.history.props.time": "时间",
|
||||||
|
|
||||||
"workflow.common.certificate.label": "证书",
|
"workflow.common.certificate.label": "证书",
|
||||||
"workflow.common.certificate.placeholder": "请选择证书来源",
|
"workflow.common.certificate.placeholder": "请选择证书来源",
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { CircleUser, Earth, History, Home, Menu, Server } from "lucide-react";
|
import { CircleUser, Home, Menu, Server, ShieldCheck, Workflow } from "lucide-react";
|
||||||
|
|
||||||
import LocaleToggle from "@/components/LocaleToggle";
|
import LocaleToggle from "@/components/LocaleToggle";
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||||
@ -58,16 +58,16 @@ export default function Dashboard() {
|
|||||||
{t("dashboard.page.title")}
|
{t("dashboard.page.title")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link to="/workflow" className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/workflow"))}>
|
<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" />
|
<Workflow className="h-4 w-4" />
|
||||||
工作流列表
|
{t("workflow.page.title")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/certificate"
|
to="/certificate"
|
||||||
className={cn("flex items-center gap-3 rounded-lg px-3 py-2 transition-all hover:text-primary", getClass("/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" />
|
<ShieldCheck className="h-4 w-4" />
|
||||||
证书列表
|
{t("certificate.page.title")}
|
||||||
</Link>
|
</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"))}>
|
||||||
@ -106,16 +106,16 @@ export default function Dashboard() {
|
|||||||
to="/workflow"
|
to="/workflow"
|
||||||
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/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" />
|
<Workflow className="h-5 w-5" />
|
||||||
工作流
|
{t("workflow.page.title")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
to="/certificate"
|
to="/certificate"
|
||||||
className={cn("mx-[-0.65rem] flex items-center gap-4 rounded-xl px-3 py-2 hover:text-foreground", getClass("/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" />
|
<ShieldCheck className="h-5 w-5" />
|
||||||
证书
|
{t("certificate.page.title")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import CertificateList from "@/components/certificate/CertificateList";
|
import CertificateList from "@/components/certificate/CertificateList";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
const Certificate = () => {
|
const Certificate = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
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">{t("certificate.page.title")}</div>
|
||||||
|
|
||||||
<CertificateList withPagination={true} />
|
<CertificateList withPagination={true} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -34,11 +34,11 @@ 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">全部证书</div>
|
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.all.certificate")}</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?.certificateTotal ? (
|
{statistic?.certificateTotal ? (
|
||||||
<Link to="/domains" className="hover:underline">
|
<Link to="/certificate" className="hover:underline">
|
||||||
{statistic?.certificateTotal}
|
{statistic?.certificateTotal}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@ -55,11 +55,11 @@ const Dashboard = () => {
|
|||||||
<CalendarClock size={48} strokeWidth={1} className="text-yellow-400" />
|
<CalendarClock size={48} strokeWidth={1} className="text-yellow-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground font-semibold">即将过期证书</div>
|
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.near_expired.certificate")}</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?.certificateExpireSoon ? (
|
{statistic?.certificateExpireSoon ? (
|
||||||
<Link to="/domains?state=expired" className="hover:underline">
|
<Link to="/certificate?state=expireSoon" className="hover:underline">
|
||||||
{statistic?.certificateExpireSoon}
|
{statistic?.certificateExpireSoon}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@ -76,11 +76,11 @@ const Dashboard = () => {
|
|||||||
<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">已过期证书</div>
|
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.expired.certificate")}</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?.certificateExpired ? (
|
{statistic?.certificateExpired ? (
|
||||||
<Link to="/domains?state=enabled" className="hover:underline">
|
<Link to="/certificate?state=expired" className="hover:underline">
|
||||||
{statistic?.certificateExpired}
|
{statistic?.certificateExpired}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@ -97,11 +97,11 @@ const Dashboard = () => {
|
|||||||
<Workflow size={48} strokeWidth={1} className="text-emerald-500" />
|
<Workflow size={48} strokeWidth={1} className="text-emerald-500" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground font-semibold">全部工作流</div>
|
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.all.workflow")}</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?.workflowTotal ? (
|
{statistic?.workflowTotal ? (
|
||||||
<Link to="/domains?state=disabled" className="hover:underline">
|
<Link to="/workflow" className="hover:underline">
|
||||||
{statistic?.workflowTotal}
|
{statistic?.workflowTotal}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@ -118,11 +118,11 @@ const Dashboard = () => {
|
|||||||
<FolderCheck size={48} strokeWidth={1} className="text-green-400" />
|
<FolderCheck size={48} strokeWidth={1} className="text-green-400" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground font-semibold">已启用工作流</div>
|
<div className="text-muted-foreground font-semibold">{t("dashboard.statistics.enabled.workflow")}</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?.workflowEnabled ? (
|
{statistic?.workflowEnabled ? (
|
||||||
<Link to="/domains?state=disabled" className="hover:underline">
|
<Link to="/workflow?state=enabled" className="hover:underline">
|
||||||
{statistic?.workflowEnabled}
|
{statistic?.workflowEnabled}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
@ -140,7 +140,7 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-muted-foreground mt-5 text-sm">最新证书</div>
|
<div className="text-muted-foreground mt-5 text-sm">{t("dashboard.certificate")}</div>
|
||||||
|
|
||||||
<CertificateList />
|
<CertificateList />
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,6 +17,7 @@ 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";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { useShallow } from "zustand/shallow";
|
import { useShallow } from "zustand/shallow";
|
||||||
@ -53,6 +54,8 @@ const WorkflowDetail = () => {
|
|||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const elements = useMemo(() => {
|
const elements = useMemo(() => {
|
||||||
let current = workflow.draft as WorkflowNode;
|
let current = workflow.draft as WorkflowNode;
|
||||||
|
|
||||||
@ -77,8 +80,8 @@ const WorkflowDetail = () => {
|
|||||||
const handleEnableChange = () => {
|
const handleEnableChange = () => {
|
||||||
if (!workflow.enabled && !allNodesValidated(workflow.draft as WorkflowNode)) {
|
if (!workflow.enabled && !allNodesValidated(workflow.draft as WorkflowNode)) {
|
||||||
toast({
|
toast({
|
||||||
title: "无法启用",
|
title: t("workflow.detail.action.save.failed"),
|
||||||
description: "有尚未设置完成的节点",
|
description: t("workflow.detail.action.save.failed.uncompleted"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -89,8 +92,8 @@ const WorkflowDetail = () => {
|
|||||||
const handleWorkflowSaveClick = () => {
|
const handleWorkflowSaveClick = () => {
|
||||||
if (!allNodesValidated(workflow.draft as WorkflowNode)) {
|
if (!allNodesValidated(workflow.draft as WorkflowNode)) {
|
||||||
toast({
|
toast({
|
||||||
title: "保存失败",
|
title: t("workflow.detail.action.save.failed"),
|
||||||
description: "有尚未设置完成的节点",
|
description: t("workflow.detail.action.save.failed.uncompleted"),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@ -113,13 +116,13 @@ const WorkflowDetail = () => {
|
|||||||
try {
|
try {
|
||||||
await run(workflow.id as string);
|
await run(workflow.id as string);
|
||||||
toast({
|
toast({
|
||||||
title: "执行成功",
|
title: t("workflow.detail.action.run.success"),
|
||||||
description: "工作流已成功执行",
|
description: t("workflow.detail.action.run.success"),
|
||||||
variant: "default",
|
variant: "default",
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
title: "执行失败",
|
title: t("workflow.detail.action.run.failed"),
|
||||||
description: getErrMessage(e),
|
description: getErrMessage(e),
|
||||||
variant: "destructive",
|
variant: "destructive",
|
||||||
});
|
});
|
||||||
@ -138,8 +141,10 @@ const WorkflowDetail = () => {
|
|||||||
<WorkflowBaseInfoEditDialog
|
<WorkflowBaseInfoEditDialog
|
||||||
trigger={
|
trigger={
|
||||||
<div className="flex flex-col space-y-1 cursor-pointer items-start">
|
<div className="flex flex-col space-y-1 cursor-pointer items-start">
|
||||||
<div className="truncate max-w-[200px]">{workflow.name ? workflow.name : "未命名工作流"}</div>
|
<div className="truncate max-w-[200px]">{workflow.name ? workflow.name : t("workflow.props.name.default")}</div>
|
||||||
<div className="text-sm text-muted-foreground truncate max-w-[200px]">{workflow.description ? workflow.description : "添加流程说明"}</div>
|
<div className="text-sm text-muted-foreground truncate max-w-[200px]">
|
||||||
|
{workflow.description ? workflow.description : t("workflow.props.description.placeholder")}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@ -152,7 +157,7 @@ const WorkflowDetail = () => {
|
|||||||
setTab("workflow");
|
setTab("workflow");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>流程</div>
|
<div>{t("workflow.detail.title")}</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn("h-full flex items-center cursor-pointer border-b-2", getTabCls("history"))}
|
className={cn("h-full flex items-center cursor-pointer border-b-2", getTabCls("history"))}
|
||||||
@ -160,7 +165,7 @@ const WorkflowDetail = () => {
|
|||||||
setTab("history");
|
setTab("history");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>历史</div>
|
<div>{t("workflow.detail.history")}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -170,12 +175,12 @@ const WorkflowDetail = () => {
|
|||||||
when={!!workflow.hasDraft}
|
when={!!workflow.hasDraft}
|
||||||
fallback={
|
fallback={
|
||||||
<Button variant={"secondary"} onClick={handleRunClick}>
|
<Button variant={"secondary"} onClick={handleRunClick}>
|
||||||
{running ? "执行中" : "立即执行"}
|
{running ? t("workflow.detail.action.running") : t("workflow.detail.action.run")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button variant={"secondary"} onClick={handleWorkflowSaveClick}>
|
<Button variant={"secondary"} onClick={handleWorkflowSaveClick}>
|
||||||
保存变更
|
{t("workflow.detail.action.save")}
|
||||||
</Button>
|
</Button>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { MoreHorizontal, Plus } from "lucide-react";
|
import { MoreHorizontal, Plus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Workflow as WorkflowType } from "@/domain/workflow";
|
import { Workflow as WorkflowType } from "@/domain/workflow";
|
||||||
import { DataTable } from "@/components/workflow/DataTable";
|
import { DataTable } from "@/components/workflow/DataTable";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { list, remove, save } from "@/repository/workflow";
|
import { list, remove, save, WorkflowListReq } from "@/repository/workflow";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
@ -28,8 +28,15 @@ const Workflow = () => {
|
|||||||
onConfirm: () => void;
|
onConfirm: () => void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const fetchData = async (page: number, pageSize?: number) => {
|
const fetchData = async (page: number, pageSize?: number) => {
|
||||||
const resp = await list({ page: page, perPage: pageSize });
|
const state = searchParams.get("state");
|
||||||
|
const req: WorkflowListReq = { page: page, perPage: pageSize };
|
||||||
|
if (state && state == "enabled") {
|
||||||
|
req.enabled = true;
|
||||||
|
}
|
||||||
|
const resp = await list(req);
|
||||||
setData(resp.items);
|
setData(resp.items);
|
||||||
setPageCount(resp.totalPages);
|
setPageCount(resp.totalPages);
|
||||||
};
|
};
|
||||||
@ -37,18 +44,18 @@ const Workflow = () => {
|
|||||||
const columns: ColumnDef<WorkflowType>[] = [
|
const columns: ColumnDef<WorkflowType>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "name",
|
accessorKey: "name",
|
||||||
header: "名称",
|
header: t("workflow.props.name"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
let name: string = row.getValue("name");
|
let name: string = row.getValue("name");
|
||||||
if (!name) {
|
if (!name) {
|
||||||
name = "未命名工作流";
|
name = t("workflow.props.name.default");
|
||||||
}
|
}
|
||||||
return <div className="max-w-[150px] truncate">{name}</div>;
|
return <div className="max-w-[150px] truncate">{name}</div>;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "description",
|
accessorKey: "description",
|
||||||
header: "描述",
|
header: t("workflow.props.description"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
let description: string = row.getValue("description");
|
let description: string = row.getValue("description");
|
||||||
if (!description) {
|
if (!description) {
|
||||||
@ -59,18 +66,18 @@ const Workflow = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "type",
|
accessorKey: "type",
|
||||||
header: "执行方式",
|
header: t("workflow.props.executionMethod"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const method = row.getValue("type");
|
const method = row.getValue("type");
|
||||||
if (!method) {
|
if (!method) {
|
||||||
return "-";
|
return "-";
|
||||||
} else if (method === "manual") {
|
} else if (method === "manual") {
|
||||||
return "手动";
|
return t("workflow.node.start.form.executionMethod.options.manual");
|
||||||
} else if (method === "auto") {
|
} else if (method === "auto") {
|
||||||
const crontab: string = row.original.crontab ?? "";
|
const crontab: string = row.original.crontab ?? "";
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<div>定时</div>
|
<div>{t("workflow.node.start.form.executionMethod.options.auto")}</div>
|
||||||
<div className="text-muted-foreground text-xs">{crontab}</div>
|
<div className="text-muted-foreground text-xs">{crontab}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -79,7 +86,7 @@ const Workflow = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "enabled",
|
accessorKey: "enabled",
|
||||||
header: "是否启用",
|
header: t("workflow.props.enabled"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const enabled: boolean = row.getValue("enabled");
|
const enabled: boolean = row.getValue("enabled");
|
||||||
|
|
||||||
@ -100,7 +107,7 @@ const Workflow = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "created",
|
accessorKey: "created",
|
||||||
header: "创建时间",
|
header: t("workflow.props.created"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const date: string = row.getValue("created");
|
const date: string = row.getValue("created");
|
||||||
return new Date(date).toLocaleString();
|
return new Date(date).toLocaleString();
|
||||||
@ -108,7 +115,7 @@ const Workflow = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "updated",
|
accessorKey: "updated",
|
||||||
header: "更新时间",
|
header: t("workflow.props.updated"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const date: string = row.getValue("updated");
|
const date: string = row.getValue("updated");
|
||||||
return new Date(date).toLocaleString();
|
return new Date(date).toLocaleString();
|
||||||
@ -128,14 +135,14 @@ const Workflow = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>操作</DropdownMenuLabel>
|
<DropdownMenuLabel>{t("workflow.action")}</DropdownMenuLabel>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
navigate(`/workflow/detail?id=${workflow.id}`);
|
navigate(`/workflow/detail?id=${workflow.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
编辑
|
{t("workflow.action.edit")}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-red-500"
|
className="text-red-500"
|
||||||
@ -169,8 +176,8 @@ const Workflow = () => {
|
|||||||
|
|
||||||
const handleDeleteClick = (id: string) => {
|
const handleDeleteClick = (id: string) => {
|
||||||
setAlertProps({
|
setAlertProps({
|
||||||
title: "删除工作流",
|
title: t("workflow.action.delete.alert.title"),
|
||||||
description: "确定删除工作流吗?",
|
description: t("workflow.action.delete.alert.description"),
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
const resp = await remove(id);
|
const resp = await remove(id);
|
||||||
if (resp) {
|
if (resp) {
|
||||||
@ -192,10 +199,10 @@ const Workflow = () => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="text-muted-foreground">工作流</div>
|
<div className="text-muted-foreground">{t("workflow.page.title")}</div>
|
||||||
<Button onClick={handleCreateClick}>
|
<Button onClick={handleCreateClick}>
|
||||||
<Plus size={16} />
|
<Plus size={16} />
|
||||||
新建工作流
|
{t("workflow.action.create")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
import { Certificate } from "@/domain/certificate";
|
import { Certificate } from "@/domain/certificate";
|
||||||
import { getPb } from "./api";
|
import { getPb } from "./api";
|
||||||
|
import { RecordListOptions } from "pocketbase";
|
||||||
|
import { getTimeAfter } from "@/lib/time";
|
||||||
|
|
||||||
type CertificateListReq = {
|
type CertificateListReq = {
|
||||||
page?: number;
|
page?: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
|
state?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const list = async (req: CertificateListReq) => {
|
export const list = async (req: CertificateListReq) => {
|
||||||
@ -19,10 +22,22 @@ export const list = async (req: CertificateListReq) => {
|
|||||||
perPage = req.perPage;
|
perPage = req.perPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = pb.collection("certificate").getList<Certificate>(page, perPage, {
|
const options: RecordListOptions = {
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
expand: "workflow",
|
expand: "workflow",
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (req.state === "expireSoon") {
|
||||||
|
options.filter = pb.filter("expireAt<{:expiredAt}", {
|
||||||
|
expiredAt: getTimeAfter(15),
|
||||||
|
});
|
||||||
|
} else if (req.state === "expired") {
|
||||||
|
options.filter = pb.filter("expireAt<={:expiredAt}", {
|
||||||
|
expiredAt: new Date(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = pb.collection("certificate").getList<Certificate>(page, perPage, options);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Workflow, WorkflowNode, WorkflowRunLog } from "@/domain/workflow";
|
import { Workflow, WorkflowNode, WorkflowRunLog } from "@/domain/workflow";
|
||||||
import { getPb } from "./api";
|
import { getPb } from "./api";
|
||||||
|
import { RecordListOptions } from "pocketbase";
|
||||||
|
|
||||||
export const get = async (id: string) => {
|
export const get = async (id: string) => {
|
||||||
const response = await getPb().collection("workflow").getOne<Workflow>(id);
|
const response = await getPb().collection("workflow").getOne<Workflow>(id);
|
||||||
@ -15,9 +16,10 @@ export const save = async (data: Record<string, string | boolean | WorkflowNode>
|
|||||||
return await getPb().collection("workflow").create<Workflow>(data);
|
return await getPb().collection("workflow").create<Workflow>(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
type WorkflowListReq = {
|
export type WorkflowListReq = {
|
||||||
page: number;
|
page: number;
|
||||||
perPage?: number;
|
perPage?: number;
|
||||||
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
export const list = async (req: WorkflowListReq) => {
|
export const list = async (req: WorkflowListReq) => {
|
||||||
let page = 1;
|
let page = 1;
|
||||||
@ -29,9 +31,17 @@ export const list = async (req: WorkflowListReq) => {
|
|||||||
perPage = req.perPage;
|
perPage = req.perPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await getPb().collection("workflow").getList<Workflow>(page, perPage, {
|
const options: RecordListOptions = {
|
||||||
sort: "-created",
|
sort: "-created",
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (req.enabled !== undefined) {
|
||||||
|
options.filter = getPb().filter("enabled={:enabled}", {
|
||||||
|
enabled: req.enabled,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await getPb().collection("workflow").getList<Workflow>(page, perPage, options);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user