diff --git a/ui/src/App.tsx b/ui/src/App.tsx index b320b71da..ddeb20e8d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -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)", }, }, diff --git a/ui/src/components/Empty.tsx b/ui/src/components/Empty.tsx new file mode 100644 index 000000000..f40edde8a --- /dev/null +++ b/ui/src/components/Empty.tsx @@ -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 ( +
+
+
+
+
+ +
+
+ {icon} +
+
+
+
+ + {isPrimitive(title) ? ( + {title} + ) : ( +

{title}

+ )} +
+ + {isPrimitive(description) ? ( + {description} + ) : ( +
{description}
+ )} +
+
+ +
{extra}
+
+
+
+
+
+
+ ); +}; + +export default Empty; diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index c6055e69d..49f06ba42 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -31,6 +31,9 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { const [modalApi, ModelContextHolder] = Modal.useModal(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const tableColumns: TableProps["columns"] = [ { key: "$index", @@ -146,9 +149,6 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); - const [page, setPage] = useState(1); - const [pageSize, setPageSize] = useState(10); - const { loading, error: loadedError, diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 7bd084604..9db5b047c 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -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?
Please be aware that this action is irreversible.", + "access.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" credential?
This action cannot be undone.", "access.props.name": "Name", "access.props.provider": "Provider", diff --git a/ui/src/i18n/locales/en/nls.certificate.json b/ui/src/i18n/locales/en/nls.certificate.json index 5a6f2a82e..fc654faec 100644 --- a/ui/src/i18n/locales/en/nls.certificate.json +++ b/ui/src/i18n/locales/en/nls.certificate.json @@ -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?
Please be aware that this action is irreversible.", + "certificate.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" certificate?
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", diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 06ab31eae..74c936480 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -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", diff --git a/ui/src/i18n/locales/en/nls.dashboard.json b/ui/src/i18n/locales/en/nls.dashboard.json index 38e20e1b7..02d3f168b 100644 --- a/ui/src/i18n/locales/en/nls.dashboard.json +++ b/ui/src/i18n/locales/en/nls.dashboard.json @@ -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", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index a1e6c53b9..304e3b69e 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -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?
Please be aware that this action is irreversible.", + "workflow.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" workflow?
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", diff --git a/ui/src/i18n/locales/en/nls.workflow.runs.json b/ui/src/i18n/locales/en/nls.workflow.runs.json index db258826c..747b0dd20 100644 --- a/ui/src/i18n/locales/en/nls.workflow.runs.json +++ b/ui/src/i18n/locales/en/nls.workflow.runs.json @@ -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?
Please be aware that this action is irreversible.", + "workflow_run.action.delete.confirm": "Are you sure want to delete this \"{{name}}\" workflow run?
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.", diff --git a/ui/src/i18n/locales/zh/nls.certificate.json b/ui/src/i18n/locales/zh/nls.certificate.json index cdac4780d..f89028ad1 100644 --- a/ui/src/i18n/locales/zh/nls.certificate.json +++ b/ui/src/i18n/locales/zh/nls.certificate.json @@ -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": "工作流", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index e6bd035a4..a88a6f579 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -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": "提交", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index bcf823bb8..c13f3dcab 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -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": "更新时间", diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index 8179fab13..7ce89c523 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -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>(() => { + return { + usage: "both-dns-hosting" satisfies AccessUsageProp, + keyword: searchParams.get("keyword"), + }; + }); + const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); + const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || 15); + const tableColumns: TableProps["columns"] = [ { key: "$index", @@ -76,7 +85,7 @@ const AccessList = () => { fixed: "right", width: 120, render: (_, record) => ( - +
{ }} /> - +
), }, ]; const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); - const [filters, setFilters] = useState>(() => { - return { - usage: "both-dns-hosting" satisfies AccessUsageProp, - keyword: searchParams.get("keyword"), - }; - }); - - const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); - const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || 15); - useEffect(() => { fetchAccesses().catch((err) => { if (err instanceof ClientResponseError && err.isAbort) { diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index 808430761..ace2e0d34 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -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>(() => { + return { + keyword: searchParams.get("keyword"), + state: searchParams.get("state"), + }; + }); + const [sorter, setSorter] = useState["onChange"]>>[2]>>(() => { + return {}; + }); + const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); + const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || 15); const tableColumns: TableProps["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["items"] = [ - ["expireSoon", "certificate.props.validity.filter.expire_soon"], - ["expired", "certificate.props.validity.filter.expired"], - ].map(([key, label]) => { - return { - key, - label: {t(label)}, - 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 ( -
- - - - - - -
- ); - }, + 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 ( - - {!isExpired ? ( - leftDays > 0 ? ( - {t("certificate.props.validity.left_days", { left: leftDays, total })} +
+ {!expired ? ( + leftDays >= 1 ? ( + +   + {t("certificate.props.validity.left_days", { left: leftDays, total })} + ) : ( - {t("certificate.props.validity.less_than_day", { hours: leftHours > 0 ? leftHours : 1 })} + +   + {t("certificate.props.validity.less_than_a_day")} + ) ) : ( - {t("certificate.props.validity.expired")} + +   + {t("certificate.props.validity.expired")} + )} {t("certificate.props.validity.expiration", { date: dayjs(record.expireAt).format("YYYY-MM-DD") })} - +
); }, }, @@ -116,10 +82,10 @@ const CertificateList = () => { key: "brand", title: t("certificate.props.brand"), render: (_, record) => ( - +
{record.issuerOrg} {record.keyAlgorithm} - +
), }, { @@ -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 ( - - {t("certificate.props.source.workflow")} - { - if (workflowId) { - navigate(`/workflows/${workflowId}`); - } - }} - > - {record.expand?.workflowId?.name ?? {t(`#${workflowId}`)}} - - - ); - } else if (record.source === CERTIFICATE_SOURCES.UPLOAD) { - return {t("certificate.props.source.upload")}; - } - - return <>; + const workflowId = record.workflowId; + return ( +
+ {t(`certificate.props.source.${record.source}`)} + { + e.stopPropagation(); + if (workflowId) { + navigate(`/workflows/${workflowId}`); + } + }} + > + {record.expand?.workflowId?.name ?? {t(`#${workflowId}`)}} + +
+ ); }, }, { @@ -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) => ( - - - + ) : ( + + ) + } + /> + ), }} 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); + }, + })} /> + + ); diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index 7b99ef352..9f3a35e04 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -30,9 +30,78 @@ const Dashboard = () => { const { t } = useTranslation(); + const breakpoints = Grid.useBreakpoint(); + + return ( +
+
+

{t("dashboard.page.title")}

+ + + + + + + + + + + + + + + + + +
+
+ ); +}; + +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 ( + +
+ {icon} + { + return {value}; + }} + suffix={{suffix}} + /> +
+
+ ); +}; + +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 ( + + + } + label={t("dashboard.statistics.all_certificates")} + loading={statisticsLoading} + value={statistics?.certificateTotal ?? "-"} + suffix={t("dashboard.statistics.unit")} + onClick={() => navigate("/certificates")} + /> + + + } + label={t("dashboard.statistics.expire_soon_certificates")} + loading={statisticsLoading} + value={statistics?.certificateExpireSoon ?? "-"} + suffix={t("dashboard.statistics.unit")} + onClick={() => navigate("/certificates?state=expireSoon")} + /> + + + } + label={t("dashboard.statistics.expired_certificates")} + loading={statisticsLoading} + value={statistics?.certificateExpired ?? "-"} + suffix={t("dashboard.statistics.unit")} + onClick={() => navigate("/certificates?state=expired")} + /> + + + } + label={t("dashboard.statistics.all_workflows")} + loading={statisticsLoading} + value={statistics?.workflowTotal ?? "-"} + suffix={t("dashboard.statistics.unit")} + onClick={() => navigate("/workflows")} + /> + + + } + label={t("dashboard.statistics.enabled_workflows")} + loading={statisticsLoading} + value={statistics?.workflowEnabled ?? "-"} + suffix={t("dashboard.statistics.unit")} + onClick={() => navigate("/workflows?state=enabled")} + /> + + + ); +}; + +const WorkflowRunHistoryTable = () => { + const { t } = useTranslation(); + const tableColumns: TableProps["columns"] = [ { key: "$index", @@ -161,127 +289,18 @@ const Dashboard = () => { ); return ( -
-
-

{t("dashboard.page.title")}

- - - - } - label={t("dashboard.statistics.all_certificates")} - loading={statisticsLoading} - value={statistics?.certificateTotal ?? "-"} - suffix={t("dashboard.statistics.unit")} - onClick={() => navigate("/certificates")} - /> - - - } - label={t("dashboard.statistics.expire_soon_certificates")} - loading={statisticsLoading} - value={statistics?.certificateExpireSoon ?? "-"} - suffix={t("dashboard.statistics.unit")} - onClick={() => navigate("/certificates?state=expireSoon")} - /> - - - } - label={t("dashboard.statistics.expired_certificates")} - loading={statisticsLoading} - value={statistics?.certificateExpired ?? "-"} - suffix={t("dashboard.statistics.unit")} - onClick={() => navigate("/certificates?state=expired")} - /> - - - } - label={t("dashboard.statistics.all_workflows")} - loading={statisticsLoading} - value={statistics?.workflowTotal ?? "-"} - suffix={t("dashboard.statistics.unit")} - onClick={() => navigate("/workflows")} - /> - - - } - label={t("dashboard.statistics.enabled_workflows")} - loading={statisticsLoading} - value={statistics?.workflowEnabled ?? "-"} - suffix={t("dashboard.statistics.unit")} - onClick={() => navigate("/workflows?state=enabled")} - /> - - - - - - - - - - - - - - - - columns={tableColumns} - dataSource={tableData} - loading={tableLoading} - locale={{ - emptyText: , - }} - pagination={false} - rowKey={(record) => record.id} - scroll={{ x: "max(100%, 720px)" }} - size="small" - /> - - -
-
- ); -}; - -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 ( - -
- {icon} - { - return {value}; - }} - suffix={{suffix}} - /> -
-
+ + columns={tableColumns} + dataSource={tableData} + loading={tableLoading} + locale={{ + emptyText: , + }} + pagination={false} + rowKey={(record) => record.id} + scroll={{ x: "max(100%, 720px)" }} + size="small" + /> ); }; diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index 30eea106d..c92cacca2 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -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>(() => { + return { + keyword: searchParams.get("keyword"), + state: searchParams.get("state"), + }; + }); + const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); + const [pageSize, setPageSize] = useState(() => parseInt(+searchParams.get("perPage")! + "") || 15); + const tableColumns: TableProps["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["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 = () => { { - handleEnabledChange(record); + handleRecordActiveChange(record); }} /> ); @@ -178,7 +186,7 @@ const WorkflowList = () => { icon={} variant="text" onClick={() => { - handleDuplicateClick(record); + handleRecordDuplicateClick(record); }} /> @@ -190,7 +198,7 @@ const WorkflowList = () => { icon={} variant="text" onClick={() => { - handleDeleteClick(record); + handleRecordDeleteClick(record); }} /> @@ -201,16 +209,6 @@ const WorkflowList = () => { const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); - const [filters, setFilters] = useState>(() => { - return { - keyword: searchParams.get("keyword"), - state: searchParams.get("state"), - }; - }); - - const [page, setPage] = useState(() => parseInt(+searchParams.get("page")! + "") || 1); - const [pageSize, setPageSize] = useState(() => 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: {t("workflow.action.delete")}, content: , diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index 486a8e210..5d7c1bb9a 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -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(page, perPage, { expand: "workflowId", filter: filters.join(" && "), - sort: "-created", + sort: sort, requestKey: null, }); }; diff --git a/ui/types/global.utility.d.ts b/ui/types/global.utility.d.ts index e7c8b65b8..3a227d63f 100644 --- a/ui/types/global.utility.d.ts +++ b/ui/types/global.utility.d.ts @@ -2,6 +2,8 @@ type Nullish = { [P in keyof T]?: T[P] | null | undefined; }; + + type ArrayElement = T extends (infer U)[] ? U : never; } export {};