mirror of
https://github.com/certimate-go/certimate.git
synced 2026-09-01 04:59:11 +08:00
feat(ui): new CertificateList page
This commit is contained in:
+4
-1
@@ -70,12 +70,15 @@ const RootApp = () => {
|
||||
Layout: {
|
||||
...antdTheme?.components?.Layout,
|
||||
bodyBg: "transparent",
|
||||
siderBg: "transparent",
|
||||
headerBg: "var(--color-container)",
|
||||
siderBg: "transparent",
|
||||
},
|
||||
Table: {
|
||||
...antdTheme?.components?.Table,
|
||||
bodySortBg: "var(--color-container)",
|
||||
headerBg: "var(--color-container)",
|
||||
headerSortActiveBg: "var(--color-container)",
|
||||
headerSortHoverBg: "var(--color-container-hover)",
|
||||
rowHoverBg: "var(--color-container-hover)",
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Typography, theme } from "antd";
|
||||
|
||||
import Show from "./Show";
|
||||
|
||||
export type EmptyProps = {
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
title?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
extra?: React.ReactNode;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
const Empty = (props: EmptyProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { className, style, title = t("common.text.nodata"), description, extra, icon } = props;
|
||||
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const isPrimitive = (node: React.ReactNode): node is string | number | boolean | null => {
|
||||
return typeof node === "string" || typeof node === "number" || typeof node === "boolean" || node == null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<div className="relative w-full overflow-hidden">
|
||||
<div className="relative top-0 left-0 z-1 flex h-full w-full py-4">
|
||||
<div className="relative w-full max-w-lg">
|
||||
<div className="flex flex-col gap-2 text-center">
|
||||
<Show when={!!icon}>
|
||||
<div className="mx-auto">
|
||||
<div className="flex size-12 items-center justify-center overflow-hidden rounded-md border text-gray-600 shadow-sm dark:text-gray-200">
|
||||
{icon}
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<div className="my-2">
|
||||
<Show when={!!title}>
|
||||
{isPrimitive(title) ? (
|
||||
<Typography.Title level={4}>{title}</Typography.Title>
|
||||
) : (
|
||||
<h4 style={{ color: themeToken.colorTextHeading }}>{title}</h4>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={!!description}>
|
||||
{isPrimitive(description) ? (
|
||||
<Typography.Text type="secondary">{description}</Typography.Text>
|
||||
) : (
|
||||
<div style={{ color: themeToken.colorTextSecondary }}>{description}</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
<Show when={!!extra}>
|
||||
<div>{extra}</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Empty;
|
||||
@@ -31,6 +31,9 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
const [modalApi, ModelContextHolder] = Modal.useModal();
|
||||
const [notificationApi, NotificationContextHolder] = notification.useNotification();
|
||||
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
|
||||
{
|
||||
key: "$index",
|
||||
@@ -146,9 +149,6 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => {
|
||||
const [tableData, setTableData] = useState<WorkflowRunModel[]>([]);
|
||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [pageSize, setPageSize] = useState<number>(10);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"access.action.edit": "Edit credential",
|
||||
"access.action.duplicate": "Duplicate credential",
|
||||
"access.action.delete": "Delete credential",
|
||||
"access.action.delete.confirm": "Are you sure want to delete \"{{name}}\" credential?<br>Please be aware that this action is irreversible.",
|
||||
"access.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" credential?<br>This action cannot be undone.",
|
||||
|
||||
"access.props.name": "Name",
|
||||
"access.props.provider": "Provider",
|
||||
|
||||
@@ -2,23 +2,26 @@
|
||||
"certificate.page.title": "Certificates",
|
||||
"certificate.page.subtitle": "SSL certificates contain the website's public key and the website's identity, along with related information. They are generated from the execution output of workflows.",
|
||||
|
||||
"certificate.nodata": "No certificates. Please create a workflow to generate certificates! 😀",
|
||||
"certificate.nodata.title": "No Certificates",
|
||||
"certificate.nodata.description": "Create a workflow to generate certificates!",
|
||||
"certificate.nodata.button": "Create workflow",
|
||||
|
||||
"certificate.search.placeholder": "Search by certificate name or serial number ...",
|
||||
|
||||
"certificate.action.view": "View certificate",
|
||||
"certificate.action.delete": "Delete certificate",
|
||||
"certificate.action.delete.confirm": "Are you sure want to delete \"{{name}}\" certificate?<br>Please be aware that this action is irreversible.",
|
||||
"certificate.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" certificate?<br>This action cannot be undone.",
|
||||
"certificate.action.download": "Download certificate",
|
||||
|
||||
"certificate.props.subject_alt_names": "Name",
|
||||
"certificate.props.validity": "Expiry",
|
||||
"certificate.props.validity.left_days": "{{left}} / {{total}} days left",
|
||||
"certificate.props.validity.less_than_day": "Expire soon ({{hours}} hours left)",
|
||||
"certificate.props.validity.less_than_a_day": "Less than 1 day",
|
||||
"certificate.props.validity.expired": "Expired",
|
||||
"certificate.props.validity.expiration": "Expire on {{date}}",
|
||||
"certificate.props.validity.filter.expire_soon": "Expire soon",
|
||||
"certificate.props.validity.filter.expired": "Expired",
|
||||
"certificate.props.validity.filters.all": "All",
|
||||
"certificate.props.validity.filters.expire_soon": "Expire soon",
|
||||
"certificate.props.validity.filters.expired": "Expired",
|
||||
"certificate.props.brand": "Brand",
|
||||
"certificate.props.source": "Source",
|
||||
"certificate.props.source.workflow": "Workflow",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"common.button.edit": "Edit",
|
||||
"common.button.more": "More",
|
||||
"common.button.ok": "Ok",
|
||||
"common.button.reload": "Reload",
|
||||
"common.button.reset": "Reset",
|
||||
"common.button.save": "Save changes",
|
||||
"common.button.submit": "Submit",
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"dashboard.statistics.expire_soon_certificates": "Expire soon certificates",
|
||||
"dashboard.statistics.expired_certificates": "Expired certificates",
|
||||
"dashboard.statistics.all_workflows": "All workflows",
|
||||
"dashboard.statistics.enabled_workflows": "Enabled workflows",
|
||||
"dashboard.statistics.enabled_workflows": "Active workflows",
|
||||
"dashboard.statistics.unit": "",
|
||||
|
||||
"dashboard.latest_workflow_runs": "Latest workflow runs",
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"workflow.action.duplicate": "Duplicate workflow",
|
||||
"workflow.action.duplicate.confirm": "Are you sure to duplicate this workflow?",
|
||||
"workflow.action.delete": "Delete workflow",
|
||||
"workflow.action.delete.confirm": "Are you sure want to delete \"{{name}}\" workflow?<br>Please be aware that this action is irreversible.",
|
||||
"workflow.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" workflow?<br>This action cannot be undone.",
|
||||
"workflow.action.enable": "Enable",
|
||||
"workflow.action.enable.failed.uncompleted": "Please complete the orchestration and publish the changes first",
|
||||
"workflow.action.disable": "Disable",
|
||||
@@ -23,8 +23,8 @@
|
||||
"workflow.props.trigger.manual": "Manual",
|
||||
"workflow.props.last_run_at": "Last run at",
|
||||
"workflow.props.state": "State",
|
||||
"workflow.props.state.filter.enabled": "Enabled",
|
||||
"workflow.props.state.filter.disabled": "Disabled",
|
||||
"workflow.props.state.filters.enabled": "Active",
|
||||
"workflow.props.state.filters.disabled": "Inactive",
|
||||
"workflow.props.created_at": "Created at",
|
||||
"workflow.props.updated_at": "Updated at",
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"workflow_run.action.cancel": "Cancel run",
|
||||
"workflow_run.action.cancel.confirm": "Are you sure to cancel this run?",
|
||||
"workflow_run.action.delete": "Delete run",
|
||||
"workflow_run.action.delete.confirm": "Are you sure want to delete \"{{name}}\" workflow run?<br>Please be aware that this action is irreversible.",
|
||||
"workflow_run.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" workflow run?<br>This action cannot be undone.",
|
||||
|
||||
"workflow_run.table.alert": "Attention: The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. It is recommended to keep it for at least 180 days.",
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
"certificate.page.title": "证书管理",
|
||||
"certificate.page.subtitle": "SSL 证书含有网站的公钥和网站标识以及其他相关信息。它们来自于工作流的执行输出。",
|
||||
|
||||
"certificate.nodata": "暂无证书,新建一个工作流去生成证书吧~ 😀",
|
||||
"certificate.nodata.title": "暂无证书",
|
||||
"certificate.nodata.description": "新建一个工作流去生成证书吧~",
|
||||
"certificate.nodata.button": "新建工作流",
|
||||
|
||||
"certificate.search.placeholder": "按证书名称或序列号搜索……",
|
||||
|
||||
@@ -14,11 +16,12 @@
|
||||
"certificate.props.subject_alt_names": "名称",
|
||||
"certificate.props.validity": "有效期限",
|
||||
"certificate.props.validity.left_days": "{{left}} / {{total}} 天",
|
||||
"certificate.props.validity.less_than_day": "即将过期(剩余 {{hours}} 小时)",
|
||||
"certificate.props.validity.expired": "已到期",
|
||||
"certificate.props.validity.expiration": "{{date}} 到期",
|
||||
"certificate.props.validity.filter.expire_soon": "即将到期",
|
||||
"certificate.props.validity.filter.expired": "已到期",
|
||||
"certificate.props.validity.less_than_a_day": "不足 1 天",
|
||||
"certificate.props.validity.expired": "已过期",
|
||||
"certificate.props.validity.expiration": "{{date}} 过期",
|
||||
"certificate.props.validity.filters.all": "全部",
|
||||
"certificate.props.validity.filters.expire_soon": "即将过期",
|
||||
"certificate.props.validity.filters.expired": "已过期",
|
||||
"certificate.props.brand": "证书品牌",
|
||||
"certificate.props.source": "来源",
|
||||
"certificate.props.source.workflow": "工作流",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"common.button.edit": "编辑",
|
||||
"common.button.more": "更多",
|
||||
"common.button.ok": "确定",
|
||||
"common.button.reload": "重新加载",
|
||||
"common.button.reset": "重置",
|
||||
"common.button.save": "保存更改",
|
||||
"common.button.submit": "提交",
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
"workflow.props.trigger.manual": "手动",
|
||||
"workflow.props.last_run_at": "最近执行时间",
|
||||
"workflow.props.state": "启用状态",
|
||||
"workflow.props.state.filter.enabled": "启用",
|
||||
"workflow.props.state.filter.disabled": "未启用",
|
||||
"workflow.props.state.filters.enabled": "启用",
|
||||
"workflow.props.state.filters.disabled": "未启用",
|
||||
"workflow.props.created_at": "创建时间",
|
||||
"workflow.props.updated_at": "更新时间",
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import { IconCopy, IconEdit, IconPlus, IconReload, IconTrash } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { App, Avatar, Button, Empty, Input, Space, Table, type TableProps, Tabs, Tooltip, Typography } from "antd";
|
||||
import { App, Avatar, Button, Empty, Input, Table, type TableProps, Tabs, Tooltip, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
@@ -27,6 +27,15 @@ const AccessList = () => {
|
||||
useZustandShallowSelector(["accesses", "loadedAtOnce", "fetchAccesses", "deleteAccess"])
|
||||
);
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
usage: "both-dns-hosting" satisfies AccessUsageProp,
|
||||
keyword: searchParams.get("keyword"),
|
||||
};
|
||||
});
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
|
||||
|
||||
const tableColumns: TableProps<AccessModel>["columns"] = [
|
||||
{
|
||||
key: "$index",
|
||||
@@ -76,7 +85,7 @@ const AccessList = () => {
|
||||
fixed: "right",
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Space.Compact>
|
||||
<div className="flex items-center justify-end">
|
||||
<AccessEditDrawer
|
||||
data={record}
|
||||
usage={filters["usage"] as AccessUsageProp}
|
||||
@@ -109,23 +118,13 @@ const AccessList = () => {
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
const [tableData, setTableData] = useState<AccessModel[]>([]);
|
||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
usage: "both-dns-hosting" satisfies AccessUsageProp,
|
||||
keyword: searchParams.get("keyword"),
|
||||
};
|
||||
});
|
||||
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccesses().catch((err) => {
|
||||
if (err instanceof ClientResponseError && err.isAbort) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { IconBrowserShare, IconReload, IconTrash } from "@tabler/icons-react";
|
||||
import { IconBrowserShare, IconCirclePlus, IconReload, IconShieldCheckeredFilled, IconTrash } from "@tabler/icons-react";
|
||||
import { useRequest } from "ahooks";
|
||||
import { App, Button, Divider, Empty, Input, Menu, type MenuProps, Radio, Space, Table, type TableProps, Tooltip, Typography, theme } from "antd";
|
||||
import { App, Button, Input, Segmented, Skeleton, Table, type TableProps, Tooltip, Typography } from "antd";
|
||||
import dayjs from "dayjs";
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer";
|
||||
import { CERTIFICATE_SOURCES, type CertificateModel } from "@/domain/certificate";
|
||||
import Empty from "@/components/Empty";
|
||||
import { type CertificateModel } from "@/domain/certificate";
|
||||
import { list as listCertificates, type ListRequest as listCertificatesRequest, remove as removeCertificate } from "@/repository/certificate";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
@@ -19,16 +20,20 @@ const CertificateList = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { modal, notification } = App.useApp();
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
keyword: searchParams.get("keyword"),
|
||||
state: searchParams.get("state"),
|
||||
};
|
||||
});
|
||||
const [sorter, setSorter] = useState<ArrayElement<Parameters<NonNullable<TableProps<CertificateModel>["onChange"]>>[2]>>(() => {
|
||||
return {};
|
||||
});
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
|
||||
|
||||
const tableColumns: TableProps<CertificateModel>["columns"] = [
|
||||
{
|
||||
key: "$index",
|
||||
align: "center",
|
||||
fixed: "left",
|
||||
width: 50,
|
||||
render: (_, __, index) => (page - 1) * pageSize + index + 1,
|
||||
},
|
||||
{
|
||||
key: "name",
|
||||
title: t("certificate.props.subject_alt_names"),
|
||||
@@ -38,77 +43,38 @@ const CertificateList = () => {
|
||||
key: "expiry",
|
||||
title: t("certificate.props.validity"),
|
||||
ellipsis: true,
|
||||
defaultFilteredValue: searchParams.has("state") ? [searchParams.get("state") as string] : undefined,
|
||||
filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => {
|
||||
const items: Required<MenuProps>["items"] = [
|
||||
["expireSoon", "certificate.props.validity.filter.expire_soon"],
|
||||
["expired", "certificate.props.validity.filter.expired"],
|
||||
].map(([key, label]) => {
|
||||
return {
|
||||
key,
|
||||
label: <Radio checked={filters["state"] === key}>{t(label)}</Radio>,
|
||||
onClick: () => {
|
||||
if (filters["state"] !== key) {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, state: key }));
|
||||
setSelectedKeys([key]);
|
||||
}
|
||||
|
||||
confirm({ closeDropdown: true });
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const handleResetClick = () => {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, state: undefined }));
|
||||
setSelectedKeys([]);
|
||||
clearFilters?.();
|
||||
confirm();
|
||||
};
|
||||
|
||||
const handleConfirmClick = () => {
|
||||
confirm();
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ padding: 0 }}>
|
||||
<Menu items={items} selectable={false} />
|
||||
<Divider className="my-0" />
|
||||
<Space className="w-full justify-end" style={{ padding: themeToken.paddingSM }}>
|
||||
<Button size="small" disabled={!filters.state} onClick={handleResetClick}>
|
||||
{t("common.button.reset")}
|
||||
</Button>
|
||||
<Button type="primary" size="small" onClick={handleConfirmClick}>
|
||||
{t("common.button.ok")}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
sorter: true,
|
||||
sortOrder: sorter.columnKey === "expiry" ? sorter.order : undefined,
|
||||
render: (_, record) => {
|
||||
const total = dayjs(record.expireAt).diff(dayjs(record.created), "d") + 1;
|
||||
// 使用 isAfter 更精确地判断是否过期
|
||||
const isExpired = dayjs().isAfter(dayjs(record.expireAt));
|
||||
const expired = dayjs().isAfter(dayjs(record.expireAt));
|
||||
const leftDays = dayjs(record.expireAt).diff(dayjs(), "d");
|
||||
const leftHours = dayjs(record.expireAt).diff(dayjs(), "h");
|
||||
|
||||
return (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
{!isExpired ? (
|
||||
leftDays > 0 ? (
|
||||
<Typography.Text type="success">{t("certificate.props.validity.left_days", { left: leftDays, total })}</Typography.Text>
|
||||
<div className="flex max-w-full flex-col gap-1">
|
||||
{!expired ? (
|
||||
leftDays >= 1 ? (
|
||||
<Typography.Text type="success">
|
||||
<span className="mr-1 inline-block size-2 rounded-full bg-success leading-2"> </span>
|
||||
{t("certificate.props.validity.left_days", { left: leftDays, total })}
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Typography.Text type="warning">{t("certificate.props.validity.less_than_day", { hours: leftHours > 0 ? leftHours : 1 })}</Typography.Text>
|
||||
<Typography.Text type="warning">
|
||||
<span className="mr-1 inline-block size-2 rounded-full bg-warning leading-2"> </span>
|
||||
{t("certificate.props.validity.less_than_a_day")}
|
||||
</Typography.Text>
|
||||
)
|
||||
) : (
|
||||
<Typography.Text type="danger">{t("certificate.props.validity.expired")}</Typography.Text>
|
||||
<Typography.Text type="danger">
|
||||
<span className="mr-1 inline-block size-2 rounded-full bg-error leading-2"> </span>
|
||||
{t("certificate.props.validity.expired")}
|
||||
</Typography.Text>
|
||||
)}
|
||||
|
||||
<Typography.Text type="secondary">
|
||||
{t("certificate.props.validity.expiration", { date: dayjs(record.expireAt).format("YYYY-MM-DD") })}
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -116,10 +82,10 @@ const CertificateList = () => {
|
||||
key: "brand",
|
||||
title: t("certificate.props.brand"),
|
||||
render: (_, record) => (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<div className="flex max-w-full flex-col gap-1">
|
||||
<Typography.Text>{record.issuerOrg}</Typography.Text>
|
||||
<Typography.Text>{record.keyAlgorithm}</Typography.Text>
|
||||
</Space>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -127,29 +93,24 @@ const CertificateList = () => {
|
||||
title: t("certificate.props.source"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
if (record.source === CERTIFICATE_SOURCES.WORKFLOW) {
|
||||
const workflowId = record.workflowId;
|
||||
return (
|
||||
<Space className="max-w-full" direction="vertical" size={4}>
|
||||
<Typography.Text>{t("certificate.props.source.workflow")}</Typography.Text>
|
||||
<Typography.Link
|
||||
type="secondary"
|
||||
ellipsis
|
||||
onClick={() => {
|
||||
if (workflowId) {
|
||||
navigate(`/workflows/${workflowId}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{record.expand?.workflowId?.name ?? <span className="font-mono">{t(`#${workflowId}`)}</span>}
|
||||
</Typography.Link>
|
||||
</Space>
|
||||
);
|
||||
} else if (record.source === CERTIFICATE_SOURCES.UPLOAD) {
|
||||
return <Typography.Text>{t("certificate.props.source.upload")}</Typography.Text>;
|
||||
}
|
||||
|
||||
return <></>;
|
||||
const workflowId = record.workflowId;
|
||||
return (
|
||||
<div className="flex max-w-full flex-col gap-1">
|
||||
<Typography.Text>{t(`certificate.props.source.${record.source}`)}</Typography.Text>
|
||||
<Typography.Link
|
||||
type="secondary"
|
||||
ellipsis
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
if (workflowId) {
|
||||
navigate(`/workflows/${workflowId}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{record.expand?.workflowId?.name ?? <span className="font-mono">{t(`#${workflowId}`)}</span>}
|
||||
</Typography.Link>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -160,65 +121,63 @@ const CertificateList = () => {
|
||||
return dayjs(record.created!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "updatedAt",
|
||||
title: t("certificate.props.updated_at"),
|
||||
ellipsis: true,
|
||||
render: (_, record) => {
|
||||
return dayjs(record.updated!).format("YYYY-MM-DD HH:mm:ss");
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "$action",
|
||||
align: "end",
|
||||
fixed: "right",
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Space.Compact>
|
||||
<CertificateDetailDrawer
|
||||
data={record}
|
||||
trigger={
|
||||
<Tooltip title={t("certificate.action.view")}>
|
||||
<Button color="primary" icon={<IconBrowserShare size="1.25em" />} variant="text" />
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
<Tooltip title={t("certificate.action.delete")}>
|
||||
<Button color="danger" icon={<IconTrash size="1.25em" />} variant="text" onClick={() => handleDeleteClick(record)} />
|
||||
<div className="flex items-center justify-end">
|
||||
<Tooltip title={t("certificate.action.view")}>
|
||||
<Button
|
||||
color="primary"
|
||||
icon={<IconBrowserShare size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRecordDetailClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space.Compact>
|
||||
<Tooltip title={t("certificate.action.delete")}>
|
||||
<Button
|
||||
color="danger"
|
||||
icon={<IconTrash size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleRecordDeleteClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
const [tableData, setTableData] = useState<CertificateModel[]>([]);
|
||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
keyword: searchParams.get("keyword"),
|
||||
state: searchParams.get("state"),
|
||||
};
|
||||
});
|
||||
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
run: refreshData,
|
||||
} = useRequest(
|
||||
() => {
|
||||
let sort: string | undefined;
|
||||
if (sorter.columnKey === "expiry") {
|
||||
sort = sorter.order === "ascend" ? "expireAt" : sorter.order === "descend" ? "-expireAt" : "";
|
||||
}
|
||||
|
||||
return listCertificates({
|
||||
keyword: filters["keyword"] as string,
|
||||
state: filters["state"] as listCertificatesRequest["state"],
|
||||
sort: sort,
|
||||
page: page,
|
||||
perPage: pageSize,
|
||||
});
|
||||
},
|
||||
{
|
||||
refreshDeps: [filters, page, pageSize],
|
||||
refreshDeps: [filters, sorter, page, pageSize],
|
||||
onSuccess: (res) => {
|
||||
setTableData(res.items);
|
||||
setTableTotal(res.totalItems);
|
||||
@@ -247,7 +206,15 @@ const CertificateList = () => {
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handleDeleteClick = (certificate: CertificateModel) => {
|
||||
const [detailRecord, setDetailRecord] = useState<CertificateModel>();
|
||||
const [detailOpen, setDetailOpen] = useState<boolean>(false);
|
||||
|
||||
const handleRecordDetailClick = (record: CertificateModel) => {
|
||||
setDetailRecord(record);
|
||||
setDetailOpen(true);
|
||||
};
|
||||
|
||||
const handleRecordDeleteClick = (certificate: CertificateModel) => {
|
||||
modal.confirm({
|
||||
title: <span className="text-error">{t("certificate.action.delete")}</span>,
|
||||
content: <span dangerouslySetInnerHTML={{ __html: t("certificate.action.delete.confirm", { name: certificate.subjectAltNames }) }} />,
|
||||
@@ -281,6 +248,22 @@ const CertificateList = () => {
|
||||
|
||||
<div className="flex items-center justify-between gap-x-2 gap-y-3 not-md:flex-col-reverse not-md:items-start not-md:justify-normal">
|
||||
<div className="flex w-full flex-1 items-center gap-x-2 md:max-w-200">
|
||||
<div>
|
||||
<Segmented
|
||||
className="shadow-xs"
|
||||
options={[
|
||||
{ label: <span className="text-sm">{t("certificate.props.validity.filters.all")}</span>, value: "" },
|
||||
{ label: <span className="text-sm">{t("certificate.props.validity.filters.expire_soon")}</span>, value: "expireSoon" },
|
||||
{ label: <span className="text-sm">{t("certificate.props.validity.filters.expired")}</span>, value: "expired" },
|
||||
]}
|
||||
size="large"
|
||||
value={(filters["state"] as string) || ""}
|
||||
onChange={(value) => {
|
||||
setPage(1);
|
||||
setFilters((prev) => ({ ...prev, state: value }));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input.Search
|
||||
className="text-sm placeholder:text-sm"
|
||||
@@ -304,7 +287,26 @@ const CertificateList = () => {
|
||||
dataSource={tableData}
|
||||
loading={loading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={getErrMsg(loadedError ?? t("certificate.nodata"))} />,
|
||||
emptyText: loading ? (
|
||||
<Skeleton />
|
||||
) : (
|
||||
<Empty
|
||||
title={t("certificate.nodata.title")}
|
||||
description={getErrMsg(loadedError ?? t("certificate.nodata.description"))}
|
||||
icon={<IconShieldCheckeredFilled size={24} />}
|
||||
extra={
|
||||
loadedError ? (
|
||||
<Button icon={<IconReload size="1.25em" />} type="primary" onClick={handleReloadClick}>
|
||||
{t("common.button.reload")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button icon={<IconCirclePlus size="1.25em" />} type="primary" onClick={() => navigate("/workflows/new")}>
|
||||
{t("workflow.action.create")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
pagination={{
|
||||
current: page,
|
||||
@@ -320,9 +322,20 @@ const CertificateList = () => {
|
||||
setPageSize(pageSize);
|
||||
},
|
||||
}}
|
||||
rowClassName="cursor-pointer"
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 960px)" }}
|
||||
onChange={(_, __, sorter) => {
|
||||
setSorter(Array.isArray(sorter) ? sorter[0] : sorter);
|
||||
}}
|
||||
onRow={(record) => ({
|
||||
onClick: () => {
|
||||
handleRecordDetailClick(record);
|
||||
},
|
||||
})}
|
||||
/>
|
||||
|
||||
<CertificateDetailDrawer data={detailRecord} open={detailOpen} onOpenChange={setDetailOpen} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,9 +30,78 @@ const Dashboard = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const breakpoints = Grid.useBreakpoint();
|
||||
|
||||
return (
|
||||
<div className="px-6 py-4">
|
||||
<div className="mx-auto max-w-320">
|
||||
<h1>{t("dashboard.page.title")}</h1>
|
||||
|
||||
<StatisticCards />
|
||||
|
||||
<Divider />
|
||||
|
||||
<Flex justify="stretch" vertical={!breakpoints.lg} gap={16}>
|
||||
<Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}>
|
||||
<Space className="w-full" direction="vertical" size="large">
|
||||
<Button block type="primary" size="large" icon={<IconPlus size="1.25em" />} onClick={() => navigate("/workflows/new")}>
|
||||
{t("dashboard.quick_actions.create_workflow")}
|
||||
</Button>
|
||||
<Button block size="large" icon={<IconUserShield size="1.25em" />} onClick={() => navigate("/settings/account")}>
|
||||
{t("dashboard.quick_actions.change_login_password")}
|
||||
</Button>
|
||||
<Button block size="large" icon={<IconPlugConnected size="1.25em" />} onClick={() => navigate("/settings/ssl-provider")}>
|
||||
{t("dashboard.quick_actions.configure_ca")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card className="flex-1" title={t("dashboard.latest_workflow_runs")}>
|
||||
<WorkflowRunHistoryTable />
|
||||
</Card>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatisticCard = ({
|
||||
label,
|
||||
loading,
|
||||
icon,
|
||||
value,
|
||||
suffix,
|
||||
onClick,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
loading?: boolean;
|
||||
icon: React.ReactNode;
|
||||
value?: string | number | React.ReactNode;
|
||||
suffix?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Card className="size-full overflow-hidden" hoverable loading={loading} variant="borderless" onClick={onClick}>
|
||||
<div className="flex gap-2">
|
||||
{icon}
|
||||
<Statistic
|
||||
title={label}
|
||||
valueRender={() => {
|
||||
return <Typography.Text className="text-4xl">{value}</Typography.Text>;
|
||||
}}
|
||||
suffix={<Typography.Text className="text-sm">{suffix}</Typography.Text>}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
const StatisticCards = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { notification } = App.useApp();
|
||||
const { token: themeToken } = theme.useToken();
|
||||
const breakpoints = Grid.useBreakpoint();
|
||||
|
||||
const statisticsGridSpans = {
|
||||
xs: { flex: "50%" },
|
||||
@@ -63,6 +132,65 @@ const Dashboard = () => {
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Row className="justify-stretch" gutter={[16, 16]}>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconShieldCheckered size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
label={t("dashboard.statistics.all_certificates")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateTotal ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconShieldExclamation size={48} strokeWidth={1} color={themeToken.colorWarning} />}
|
||||
label={t("dashboard.statistics.expire_soon_certificates")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateExpireSoon ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates?state=expireSoon")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconShieldX size={48} strokeWidth={1} color={themeToken.colorError} />}
|
||||
label={t("dashboard.statistics.expired_certificates")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateExpired ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates?state=expired")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconSchema size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
label={t("dashboard.statistics.all_workflows")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.workflowTotal ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/workflows")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconActivity size={48} strokeWidth={1} color={themeToken.colorSuccess} />}
|
||||
label={t("dashboard.statistics.enabled_workflows")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.workflowEnabled ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/workflows?state=enabled")}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
const WorkflowRunHistoryTable = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const tableColumns: TableProps<WorkflowRunModel>["columns"] = [
|
||||
{
|
||||
key: "$index",
|
||||
@@ -161,127 +289,18 @@ const Dashboard = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="px-6 py-4">
|
||||
<div className="mx-auto max-w-320">
|
||||
<h1>{t("dashboard.page.title")}</h1>
|
||||
|
||||
<Row className="justify-stretch" gutter={[16, 16]}>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconShieldCheckered size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
label={t("dashboard.statistics.all_certificates")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateTotal ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconShieldExclamation size={48} strokeWidth={1} color={themeToken.colorWarning} />}
|
||||
label={t("dashboard.statistics.expire_soon_certificates")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateExpireSoon ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates?state=expireSoon")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconShieldX size={48} strokeWidth={1} color={themeToken.colorError} />}
|
||||
label={t("dashboard.statistics.expired_certificates")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.certificateExpired ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/certificates?state=expired")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconSchema size={48} strokeWidth={1} color={themeToken.colorInfo} />}
|
||||
label={t("dashboard.statistics.all_workflows")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.workflowTotal ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/workflows")}
|
||||
/>
|
||||
</Col>
|
||||
<Col {...statisticsGridSpans}>
|
||||
<StatisticCard
|
||||
icon={<IconActivity size={48} strokeWidth={1} color={themeToken.colorSuccess} />}
|
||||
label={t("dashboard.statistics.enabled_workflows")}
|
||||
loading={statisticsLoading}
|
||||
value={statistics?.workflowEnabled ?? "-"}
|
||||
suffix={t("dashboard.statistics.unit")}
|
||||
onClick={() => navigate("/workflows?state=enabled")}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Flex justify="stretch" vertical={!breakpoints.lg} gap={16}>
|
||||
<Card className="max-lg:flex-1 lg:w-[360px]" title={t("dashboard.quick_actions")}>
|
||||
<Space className="w-full" direction="vertical" size="large">
|
||||
<Button block type="primary" size="large" icon={<IconPlus size="1.25em" />} onClick={() => navigate("/workflows/new")}>
|
||||
{t("dashboard.quick_actions.create_workflow")}
|
||||
</Button>
|
||||
<Button block size="large" icon={<IconUserShield size="1.25em" />} onClick={() => navigate("/settings/account")}>
|
||||
{t("dashboard.quick_actions.change_login_password")}
|
||||
</Button>
|
||||
<Button block size="large" icon={<IconPlugConnected size="1.25em" />} onClick={() => navigate("/settings/ssl-provider")}>
|
||||
{t("dashboard.quick_actions.configure_ca")}
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
<Card className="flex-1" title={t("dashboard.latest_workflow_runs")}>
|
||||
<Table<WorkflowRunModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={tableLoading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 720px)" }}
|
||||
size="small"
|
||||
/>
|
||||
</Card>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const StatisticCard = ({
|
||||
label,
|
||||
loading,
|
||||
icon,
|
||||
value,
|
||||
suffix,
|
||||
onClick,
|
||||
}: {
|
||||
label: React.ReactNode;
|
||||
loading?: boolean;
|
||||
icon: React.ReactNode;
|
||||
value?: string | number | React.ReactNode;
|
||||
suffix?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
}) => {
|
||||
return (
|
||||
<Card className="size-full overflow-hidden" hoverable loading={loading} variant="borderless" onClick={onClick}>
|
||||
<div className="flex gap-2">
|
||||
{icon}
|
||||
<Statistic
|
||||
title={label}
|
||||
valueRender={() => {
|
||||
return <Typography.Text className="text-4xl">{value}</Typography.Text>;
|
||||
}}
|
||||
suffix={<Typography.Text className="text-sm">{suffix}</Typography.Text>}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
<Table<WorkflowRunModel>
|
||||
columns={tableColumns}
|
||||
dataSource={tableData}
|
||||
loading={tableLoading}
|
||||
locale={{
|
||||
emptyText: <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />,
|
||||
}}
|
||||
pagination={false}
|
||||
rowKey={(record) => record.id}
|
||||
scroll={{ x: "max(100%, 720px)" }}
|
||||
size="small"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import { ClientResponseError } from "pocketbase";
|
||||
|
||||
import WorkflowStatusIcon from "@/components/workflow/WorkflowStatusIcon";
|
||||
import { WORKFLOW_TRIGGERS, type WorkflowModel, cloneNode, initWorkflow, isAllNodesValidated } from "@/domain/workflow";
|
||||
import { WORKFLOW_RUN_STATUSES } from "@/domain/workflowRun";
|
||||
import { list as listWorkflows, remove as removeWorkflow, save as saveWorkflow } from "@/repository/workflow";
|
||||
import { getErrMsg } from "@/utils/error";
|
||||
|
||||
@@ -22,6 +21,15 @@ const WorkflowList = () => {
|
||||
const { message, modal, notification } = App.useApp();
|
||||
const { token: themeToken } = theme.useToken();
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
keyword: searchParams.get("keyword"),
|
||||
state: searchParams.get("state"),
|
||||
};
|
||||
});
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
|
||||
|
||||
const tableColumns: TableProps<WorkflowModel>["columns"] = [
|
||||
{
|
||||
key: "$index",
|
||||
@@ -69,8 +77,8 @@ const WorkflowList = () => {
|
||||
defaultFilteredValue: searchParams.has("state") ? [searchParams.get("state") as string] : undefined,
|
||||
filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => {
|
||||
const items: Required<MenuProps>["items"] = [
|
||||
["enabled", "workflow.props.state.filter.enabled"],
|
||||
["disabled", "workflow.props.state.filter.disabled"],
|
||||
["enabled", "workflow.props.state.filters.enabled"],
|
||||
["disabled", "workflow.props.state.filters.disabled"],
|
||||
].map(([key, label]) => {
|
||||
return {
|
||||
key,
|
||||
@@ -120,7 +128,7 @@ const WorkflowList = () => {
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={() => {
|
||||
handleEnabledChange(record);
|
||||
handleRecordActiveChange(record);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -178,7 +186,7 @@ const WorkflowList = () => {
|
||||
icon={<IconCopy size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
handleDuplicateClick(record);
|
||||
handleRecordDuplicateClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -190,7 +198,7 @@ const WorkflowList = () => {
|
||||
icon={<IconTrash size="1.25em" />}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
handleDeleteClick(record);
|
||||
handleRecordDeleteClick(record);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -201,16 +209,6 @@ const WorkflowList = () => {
|
||||
const [tableData, setTableData] = useState<WorkflowModel[]>([]);
|
||||
const [tableTotal, setTableTotal] = useState<number>(0);
|
||||
|
||||
const [filters, setFilters] = useState<Record<string, unknown>>(() => {
|
||||
return {
|
||||
keyword: searchParams.get("keyword"),
|
||||
state: searchParams.get("state"),
|
||||
};
|
||||
});
|
||||
|
||||
const [page, setPage] = useState<number>(() => parseInt(+searchParams.get("page")! + "") || 1);
|
||||
const [pageSize, setPageSize] = useState<number>(() => parseInt(+searchParams.get("perPage")! + "") || 15);
|
||||
|
||||
const {
|
||||
loading,
|
||||
error: loadedError,
|
||||
@@ -258,7 +256,7 @@ const WorkflowList = () => {
|
||||
refreshData();
|
||||
};
|
||||
|
||||
const handleEnabledChange = async (workflow: WorkflowModel) => {
|
||||
const handleRecordActiveChange = async (workflow: WorkflowModel) => {
|
||||
try {
|
||||
if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) {
|
||||
message.warning(t("workflow.action.enable.failed.uncompleted"));
|
||||
@@ -285,7 +283,7 @@ const WorkflowList = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDuplicateClick = (workflow: WorkflowModel) => {
|
||||
const handleRecordDuplicateClick = (workflow: WorkflowModel) => {
|
||||
modal.confirm({
|
||||
title: t("workflow.action.duplicate"),
|
||||
content: t("workflow.action.duplicate.confirm"),
|
||||
@@ -315,7 +313,7 @@ const WorkflowList = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteClick = (workflow: WorkflowModel) => {
|
||||
const handleRecordDeleteClick = (workflow: WorkflowModel) => {
|
||||
modal.confirm({
|
||||
title: <span className="text-error">{t("workflow.action.delete")}</span>,
|
||||
content: <span dangerouslySetInnerHTML={{ __html: t("workflow.action.delete.confirm", { name: workflow.name }) }} />,
|
||||
|
||||
@@ -6,6 +6,7 @@ import { COLLECTION_NAME_CERTIFICATE, getPocketBase } from "./_pocketbase";
|
||||
export type ListRequest = {
|
||||
keyword?: string;
|
||||
state?: "expireSoon" | "expired";
|
||||
sort?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
};
|
||||
@@ -23,12 +24,14 @@ export const list = async (request: ListRequest) => {
|
||||
filters.push(pb.filter("expireAt<={:expiredAt}", { expiredAt: new Date() }));
|
||||
}
|
||||
|
||||
const sort = request.sort || "-created";
|
||||
|
||||
const page = request.page || 1;
|
||||
const perPage = request.perPage || 10;
|
||||
return pb.collection(COLLECTION_NAME_CERTIFICATE).getList<CertificateModel>(page, perPage, {
|
||||
expand: "workflowId",
|
||||
filter: filters.join(" && "),
|
||||
sort: "-created",
|
||||
sort: sort,
|
||||
requestKey: null,
|
||||
});
|
||||
};
|
||||
|
||||
Vendored
+2
@@ -2,6 +2,8 @@
|
||||
type Nullish<T> = {
|
||||
[P in keyof T]?: T[P] | null | undefined;
|
||||
};
|
||||
|
||||
type ArrayElement<T> = T extends (infer U)[] ? U : never;
|
||||
}
|
||||
|
||||
export {};
|
||||
|
||||
Reference in New Issue
Block a user