diff --git a/ui/src/components/certificate/CertificateList.tsx b/ui/src/components/certificate/CertificateList.tsx index a6a9a86a..458186fd 100644 --- a/ui/src/components/certificate/CertificateList.tsx +++ b/ui/src/components/certificate/CertificateList.tsx @@ -66,16 +66,12 @@ const CertificateList = ({ withPagination }: CertificateListProps) => { return (
{leftDays > 0 ? ( -
- {leftDays} / {allDays} {t("certificate.props.expiry.days")} -
+
{t("certificate.props.expiry.left_days", { left: leftDays, total: allDays })}
) : (
{t("certificate.props.expiry.expired")}
)} -
- {new Date(expireAt).toLocaleString().split(" ")[0]} {t("certificate.props.expiry.text.expire")} -
+
{t("certificate.props.expiry.expiration", { date: new Date(expireAt).toLocaleString().split(" ")[0] })}
); }, diff --git a/ui/src/i18n/locales/en/nls.certificate.json b/ui/src/i18n/locales/en/nls.certificate.json index fc80b7bd..0599e7e8 100644 --- a/ui/src/i18n/locales/en/nls.certificate.json +++ b/ui/src/i18n/locales/en/nls.certificate.json @@ -8,9 +8,11 @@ "certificate.props.domain": "Name", "certificate.props.expiry": "Expiry", - "certificate.props.expiry.days": "Days", + "certificate.props.expiry.left_days": "{{left}} / {{total}} days left", "certificate.props.expiry.expired": "Expired", - "certificate.props.expiry.text.expire": "Expire", + "certificate.props.expiry.expiration": "Expire on {{date}}", + "certificate.props.expiry.filter.expire_soon": "Expire Soon", + "certificate.props.expiry.filter.expired": "Expired", "certificate.props.workflow": "Workflow", "certificate.props.source": "Source", "certificate.props.certificate_chain": "Certificate Chain", diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index d8ffa44e..bdbe27eb 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -11,6 +11,7 @@ "common.delete.succeeded.message": "Delete Successful", "common.delete.failed.message": "Delete Failed", "common.next": "Next", + "common.reset": "Reset", "common.confirm": "Confirm", "common.cancel": "Cancel", "common.submit": "Submit", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 6307e434..b4ff4f01 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -21,7 +21,9 @@ "workflow.props.description": "Description", "workflow.props.description.placeholder": "Please enter description", "workflow.props.executionMethod": "Execution Method", - "workflow.props.enabled": "Enabled", + "workflow.props.state": "State", + "workflow.props.state.filter.enabled": "Enabled", + "workflow.props.state.filter.disabled": "Disabled", "workflow.props.createdAt": "Created", "workflow.props.updatedAt": "Updated", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 6c33bf52..f4862fbb 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -1,7 +1,7 @@ { "access.page.title": "授权管理", - "access.nodata": "暂无授权信息,请先创建。", + "access.nodata": "暂无授权信息,请先新建", "access.action.add": "新建授权", "access.action.edit": "编辑授权", diff --git a/ui/src/i18n/locales/zh/nls.certificate.json b/ui/src/i18n/locales/zh/nls.certificate.json index b4bb60de..e9274c5f 100644 --- a/ui/src/i18n/locales/zh/nls.certificate.json +++ b/ui/src/i18n/locales/zh/nls.certificate.json @@ -1,16 +1,18 @@ { "certificate.page.title": "证书管理", - "certificate.nodata": "暂无证书,创建一个工作流去生成证书吧~ 😀", + "certificate.nodata": "暂无证书,新建一个工作流去生成证书吧~ 😀", "certificate.action.view": "查看证书", "certificate.action.download": "下载证书", "certificate.props.domain": "名称", "certificate.props.expiry": "有效期限", - "certificate.props.expiry.days": "天", + "certificate.props.expiry.left_days": "{{left}} / {{total}} 天", "certificate.props.expiry.expired": "已到期", - "certificate.props.expiry.text.expire": "到期", + "certificate.props.expiry.expiration": "{{date}} 到期", + "certificate.props.expiry.filter.expire_soon": "即将到期", + "certificate.props.expiry.filter.expired": "已到期", "certificate.props.workflow": "所属工作流", "certificate.props.source": "来源", "certificate.props.certificate_chain": "证书内容", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 96594aab..e78df39e 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -11,6 +11,7 @@ "common.delete.succeeded.message": "删除成功", "common.delete.failed.message": "删除失败", "common.next": "下一步", + "common.reset": "重置", "common.confirm": "确认", "common.cancel": "取消", "common.submit": "提交", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 99f4fc71..ae323d6b 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -1,9 +1,7 @@ { "workflow.page.title": "工作流", - "certificate.nodata": "暂无证书,创建一个工作流去生成证书吧~ 😀", - - "workflow.nodata": "No workflows yet. Try to create a workflow to generate certificates! 😀", + "workflow.nodata": "暂无工作流,请先新建", "workflow.detail.title": "流程", "workflow.detail.history": "历史", @@ -23,7 +21,9 @@ "workflow.props.description": "描述", "workflow.props.description.placeholder": "请输入描述", "workflow.props.executionMethod": "执行方式", - "workflow.props.enabled": "是否启用", + "workflow.props.state": "启用状态", + "workflow.props.state.filter.enabled": "启用", + "workflow.props.state.filter.disabled": "未启用", "workflow.props.createdAt": "创建时间", "workflow.props.updatedAt": "更新时间", diff --git a/ui/src/lib/time.ts b/ui/src/lib/time.ts index c86ce1e2..0d737f65 100644 --- a/ui/src/lib/time.ts +++ b/ui/src/lib/time.ts @@ -16,11 +16,6 @@ export const convertZulu2Beijing = (zuluTime: string) => { return formattedBeijingTime; }; -export const getDate = (zuluTime: string) => { - const time = convertZulu2Beijing(zuluTime); - return time.split(" ")[0]; -}; - export const getLeftDays = (zuluTime: string) => { const time = convertZulu2Beijing(zuluTime); const date = time.split(" ")[0]; @@ -38,49 +33,3 @@ export const diffDays = (date1: string, date2: string) => { const days = Math.ceil(diff / (1000 * 60 * 60 * 24)); return days; }; - -export function getTimeBefore(days: number): string { - // 获取当前时间 - const currentDate = new Date(); - - // 减去指定的天数 - currentDate.setUTCDate(currentDate.getUTCDate() - days); - - // 格式化日期为 yyyy-mm-dd - const year = currentDate.getUTCFullYear(); - const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始 - const day = String(currentDate.getUTCDate()).padStart(2, "0"); - - // 格式化时间为 hh:ii:ss - const hours = String(currentDate.getUTCHours()).padStart(2, "0"); - const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0"); - const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0"); - - // 组合成 yyyy-mm-dd hh:ii:ss 格式 - const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; - - return formattedDate; -} - -export function getTimeAfter(days: number): string { - // 获取当前时间 - const currentDate = new Date(); - - // 加上指定的天数 - currentDate.setUTCDate(currentDate.getUTCDate() + days); - - // 格式化日期为 yyyy-mm-dd - const year = currentDate.getUTCFullYear(); - const month = String(currentDate.getUTCMonth() + 1).padStart(2, "0"); // 月份从 0 开始 - const day = String(currentDate.getUTCDate()).padStart(2, "0"); - - // 格式化时间为 hh:ii:ss - const hours = String(currentDate.getUTCHours()).padStart(2, "0"); - const minutes = String(currentDate.getUTCMinutes()).padStart(2, "0"); - const seconds = String(currentDate.getUTCSeconds()).padStart(2, "0"); - - // 组合成 yyyy-mm-dd hh:ii:ss 格式 - const formattedDate = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; - - return formattedDate; -} diff --git a/ui/src/pages/ConsoleLayout.tsx b/ui/src/pages/ConsoleLayout.tsx index b385ac73..197cc9d9 100644 --- a/ui/src/pages/ConsoleLayout.tsx +++ b/ui/src/pages/ConsoleLayout.tsx @@ -24,9 +24,7 @@ const ConsoleLayout = () => { const { t } = useTranslation(); - const { - token: { colorBgContainer }, - } = theme.useToken(); + const { token: themeToken } = theme.useToken(); const menuItems: Required["items"] = [ { @@ -56,10 +54,15 @@ const ConsoleLayout = () => { ]; const [menuSelectedKey, setMenuSelectedKey] = useState(); - useEffect(() => { + const getActiveMenuItem = () => { const item = menuItems.find((item) => item!.key === location.pathname) ?? menuItems.find((item) => item!.key !== "/" && location.pathname.startsWith(item!.key as string)); + return item; + }; + + useEffect(() => { + const item = getActiveMenuItem(); if (item) { setMenuSelectedKey(item.key as string); } else { @@ -68,7 +71,7 @@ const ConsoleLayout = () => { }, [location.pathname]); useEffect(() => { - if (menuSelectedKey) { + if (menuSelectedKey && menuSelectedKey !== getActiveMenuItem()?.key) { navigate(menuSelectedKey); } }, [menuSelectedKey]); @@ -116,7 +119,7 @@ const ConsoleLayout = () => { - +
{/*
diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index 75b12ecc..543c2f3b 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, Button, Empty, Modal, notification, Space, Table, Tooltip, Typography, type TableProps } from "antd"; import { PageHeader } from "@ant-design/pro-components"; @@ -13,9 +13,6 @@ import { useConfigContext } from "@/providers/config"; const AccessList = () => { const { t } = useTranslation(); - // a flag to fix the twice-rendering issue in strict mode - const mountRef = useRef(true); - const [modalApi, ModelContextHolder] = Modal.useModal(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); @@ -134,11 +131,6 @@ const AccessList = () => { }, [page, pageSize, configContext.config.accesses]); useEffect(() => { - if (mountRef.current) { - mountRef.current = false; - return; - } - fetchTableData(); }, [fetchTableData]); diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index fccb1be0..5e91ce33 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -1,15 +1,14 @@ -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { Button, Empty, notification, Space, Table, Tooltip, Typography, type TableProps } from "antd"; +import { Button, Divider, Empty, Menu, notification, Radio, Space, Table, theme, Tooltip, Typography, type MenuProps, type TableProps } from "antd"; import { PageHeader } from "@ant-design/pro-components"; -import { Eye as EyeIcon } from "lucide-react"; +import { Eye as EyeIcon, Filter as FilterIcon } from "lucide-react"; import moment from "moment"; import CertificateDetailDrawer from "@/components/certificate/CertificateDetailDrawer"; import { Certificate as CertificateType } from "@/domain/certificate"; import { list as listCertificate, type CertificateListReq } from "@/repository/certificate"; -import { diffDays, getLeftDays } from "@/lib/time"; const CertificateList = () => { const navigate = useNavigate(); @@ -17,8 +16,7 @@ const CertificateList = () => { const { t } = useTranslation(); - // a flag to fix the twice-rendering issue in strict mode - const mountRef = useRef(true); + const { token: themeToken } = theme.useToken(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); @@ -40,21 +38,65 @@ const CertificateList = () => { { key: "expiry", title: t("certificate.props.expiry"), + filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => { + const items: Required["items"] = [ + ["expireSoon", "certificate.props.expiry.filter.expire_soon"], + ["expired", "certificate.props.expiry.filter.expired"], + ].map(([key, label]) => { + return { + key, + label: {t(label)}, + onClick: () => { + if (filters["state"] !== key) { + setFilters((prev) => ({ ...prev, state: key })); + setSelectedKeys([key]); + } + + confirm({ closeDropdown: true }); + }, + }; + }); + + const handleResetClick = () => { + setFilters((prev) => ({ ...prev, state: undefined })); + setSelectedKeys([]); + clearFilters?.(); + confirm(); + }; + + const handleConfirmClick = () => { + confirm(); + }; + + return ( +
+ + + + + + +
+ ); + }, + filterIcon: () => , render: (_, record) => { - const leftDays = getLeftDays(record.expireAt); - const allDays = diffDays(record.expireAt, record.created); + const total = moment(record.expireAt).diff(moment(record.created), "d") + 1; + const left = moment(record.expireAt).diff(moment(), "d"); return ( - {leftDays > 0 ? ( - - {leftDays} / {allDays} {t("certificate.props.expiry.days")} - + {left > 0 ? ( + {t("certificate.props.expiry.left_days", { left, total })} ) : ( {t("certificate.props.expiry.expired")} )} - {moment(record.expireAt).format("YYYY-MM-DD")} {t("certificate.props.expiry.text.expire")} + {t("certificate.props.expiry.expiration", { date: moment(record.expireAt).format("YYYY-MM-DD") })} ); @@ -122,21 +164,31 @@ const CertificateList = () => { const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); + const [filters, setFilters] = useState>({}); + const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); + const [currentRecord, setCurrentRecord] = useState(); + + const [drawerOpen, setDrawerOpen] = useState(false); + + useEffect(() => { + setFilters({ ...filters, state: searchParams.get("state") }); + setPage(parseInt(+searchParams.get("page")! + "") || 1); + setPageSize(parseInt(+searchParams.get("perPage")! + "") || 10); + }, []); + const fetchTableData = useCallback(async () => { if (loading) return; setLoading(true); - const state = searchParams.get("state"); - const req: CertificateListReq = { page: page, perPage: pageSize }; - if (state) { - req.state = state as CertificateListReq["state"]; - } - try { - const resp = await listCertificate(req); + const resp = await listCertificate({ + page: page, + perPage: pageSize, + state: filters["state"] as CertificateListReq["state"], + }); setTableData(resp.items); setTableTotal(resp.totalItems); @@ -146,20 +198,12 @@ const CertificateList = () => { } finally { setLoading(false); } - }, [page, pageSize]); + }, [filters, page, pageSize]); useEffect(() => { - if (mountRef.current) { - mountRef.current = false; - return; - } - fetchTableData(); }, [fetchTableData]); - const [drawerOpen, setDrawerOpen] = useState(false); - const [currentRecord, setCurrentRecord] = useState(); - const handleViewClick = (certificate: CertificateType) => { setDrawerOpen(true); setCurrentRecord(certificate); @@ -194,6 +238,10 @@ const CertificateList = () => { }, }} rowKey={(record) => record.id} + onChange={(_, filters, __, extra) => { + console.log(filters); + extra.action === "filter" && fetchTableData(); + }} /> { const navigate = useNavigate(); @@ -15,8 +31,7 @@ const WorkflowList = () => { const { t } = useTranslation(); - // a flag to fix the twice-rendering issue in strict mode - const mountRef = useRef(true); + const { token: themeToken } = theme.useToken(); const [modalApi, ModelContextHolder] = Modal.useModal(); const [notificationApi, NotificationContextHolder] = notification.useNotification(); @@ -63,19 +78,63 @@ const WorkflowList = () => { }, }, { - key: "enabled", - title: t("workflow.props.enabled"), + key: "state", + title: t("workflow.props.state"), + filterDropdown: ({ setSelectedKeys, confirm, clearFilters }) => { + const items: Required["items"] = [ + ["enabled", "workflow.props.state.filter.enabled"], + ["disabled", "workflow.props.state.filter.disabled"], + ].map(([key, label]) => { + return { + key, + label: {t(label)}, + onClick: () => { + if (filters["state"] !== key) { + setFilters((prev) => ({ ...prev, state: key })); + setSelectedKeys([key]); + } + + confirm({ closeDropdown: true }); + }, + }; + }); + + const handleResetClick = () => { + setFilters((prev) => ({ ...prev, state: undefined })); + setSelectedKeys([]); + clearFilters?.(); + confirm(); + }; + + const handleConfirmClick = () => { + confirm(); + }; + + return ( +
+ + + + + + +
+ ); + }, + filterIcon: () => , render: (_, record) => { const enabled = record.enabled; return ( - <> - { - handleEnabledChange(record); - }} - /> - + { + handleEnabledChange(record); + }} + /> ); }, }, @@ -136,22 +195,27 @@ const WorkflowList = () => { const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); + const [filters, setFilters] = useState>({}); + const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); - // TODO: 表头筛选 + useEffect(() => { + setFilters({ ...filters, state: searchParams.get("state") }); + setPage(parseInt(+searchParams.get("page")! + "") || 1); + setPageSize(parseInt(+searchParams.get("perPage")! + "") || 10); + }, []); + const fetchTableData = useCallback(async () => { if (loading) return; setLoading(true); - const state = searchParams.get("state"); - const req: WorkflowListReq = { page: page, perPage: pageSize }; - if (state == "enabled") { - req.enabled = true; - } - try { - const resp = await listWorkflow(req); + const resp = await listWorkflow({ + page: page, + perPage: pageSize, + enabled: (filters["state"] as string) === "enabled" ? true : (filters["state"] as string) === "disabled" ? false : undefined, + }); setTableData(resp.items); setTableTotal(resp.totalItems); @@ -161,14 +225,9 @@ const WorkflowList = () => { } finally { setLoading(false); } - }, [searchParams, page, pageSize]); + }, [filters, page, pageSize]); useEffect(() => { - if (mountRef.current) { - mountRef.current = false; - return; - } - fetchTableData(); }, [fetchTableData]); diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index 9bafc0f7..ee911377 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -1,7 +1,7 @@ import { type RecordListOptions } from "pocketbase"; +import moment from "moment"; import { type Certificate } from "@/domain/certificate"; -import { getTimeAfter } from "@/lib/time"; import { getPocketBase } from "./pocketbase"; export type CertificateListReq = { @@ -23,7 +23,7 @@ export const list = async (req: CertificateListReq) => { if (req.state === "expireSoon") { options.filter = pb.filter("expireAt<{:expiredAt}", { - expiredAt: getTimeAfter(15), + expiredAt: moment().add(15, "d").toDate(), }); } else if (req.state === "expired") { options.filter = pb.filter("expireAt<={:expiredAt}", {