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}", {