improve multi language

This commit is contained in:
yoan 2024-11-23 12:55:31 +08:00
parent 47050769fc
commit 37df882ed3
22 changed files with 291 additions and 105 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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"
}

View File

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

View File

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

View File

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

View 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": "下载证书"
}

View File

@ -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": "最新证书"
} }

View File

@ -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": "请选择证书来源",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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