From 664bb692b6ecf4c005401dd912b054edd7b5647e Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 13 Feb 2025 20:31:11 +0800 Subject: [PATCH] feat: search by keyword on AccessList, CertificateList, WorkflowList --- .../workflow/node/DeployNodeConfigForm.tsx | 9 +- ui/src/i18n/locales/en/nls.access.json | 2 + ui/src/i18n/locales/en/nls.certificate.json | 2 + ui/src/i18n/locales/en/nls.workflow.json | 2 + ui/src/i18n/locales/zh/nls.access.json | 2 + ui/src/i18n/locales/zh/nls.certificate.json | 2 + ui/src/i18n/locales/zh/nls.workflow.json | 2 + ui/src/pages/accesses/AccessList.tsx | 98 +++++++++++++------ ui/src/pages/certificates/CertificateList.tsx | 97 +++++++++++++----- ui/src/pages/workflows/WorkflowList.tsx | 79 ++++++++++----- ui/src/repository/access.ts | 7 +- ui/src/repository/certificate.ts | 50 +++++----- ui/src/repository/workflow.ts | 27 ++--- ui/src/repository/workflowRun.ts | 25 +++-- ui/src/stores/access/index.ts | 2 +- 15 files changed, 262 insertions(+), 144 deletions(-) diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 92ce46cc..c5518057 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -212,7 +212,6 @@ const DeployNodeConfigForm = forwardRef { if (fieldProvider === value) return; - // TODO: 暂时不支持切换部署目标,需后端调整,否则之前若存在部署结果输出就不会再部署 // 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标 if (initialValues?.provider === value) { formInst.resetFields(); @@ -276,13 +275,7 @@ const DeployNodeConfigForm = forwardRef} > - + diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 25f8765d..af3676b3 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -3,6 +3,8 @@ "access.nodata": "No accesses. Please create an authorization first.", + "access.search.placeholder": "Search by access name ...", + "access.action.add": "Create authorization", "access.action.edit": "Edit authorization", "access.action.duplicate": "Duplicate authorization", diff --git a/ui/src/i18n/locales/en/nls.certificate.json b/ui/src/i18n/locales/en/nls.certificate.json index 7557daff..cca7fdb4 100644 --- a/ui/src/i18n/locales/en/nls.certificate.json +++ b/ui/src/i18n/locales/en/nls.certificate.json @@ -3,6 +3,8 @@ "certificate.nodata": "No certificates. Please create a workflow to generate certificates! 😀", + "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 to delete this certificate?", diff --git a/ui/src/i18n/locales/en/nls.workflow.json b/ui/src/i18n/locales/en/nls.workflow.json index 465923d1..b4f9d7e6 100644 --- a/ui/src/i18n/locales/en/nls.workflow.json +++ b/ui/src/i18n/locales/en/nls.workflow.json @@ -3,6 +3,8 @@ "workflow.nodata": "No workflows. Please create a workflow to generate certificates! 😀", + "workflow.search.placeholder": "Search by workflow name ...", + "workflow.action.create": "Create workflow", "workflow.action.edit": "Edit workflow", "workflow.action.delete": "Delete workflow", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 3581a527..37ebb112 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -3,6 +3,8 @@ "access.nodata": "暂无授权信息,请先新建授权", + "access.search.placeholder": "按授权名称搜索……", + "access.action.add": "新建授权", "access.action.edit": "编辑授权", "access.action.duplicate": "复制授权", diff --git a/ui/src/i18n/locales/zh/nls.certificate.json b/ui/src/i18n/locales/zh/nls.certificate.json index b4e9bee0..3c55e660 100644 --- a/ui/src/i18n/locales/zh/nls.certificate.json +++ b/ui/src/i18n/locales/zh/nls.certificate.json @@ -3,6 +3,8 @@ "certificate.nodata": "暂无证书,新建一个工作流去生成证书吧~ 😀", + "certificate.search.placeholder": "按证书名称或序列号搜索……", + "certificate.action.view": "查看证书", "certificate.action.delete": "删除证书", "certificate.action.delete.confirm": "确定要删除此证书吗?", diff --git a/ui/src/i18n/locales/zh/nls.workflow.json b/ui/src/i18n/locales/zh/nls.workflow.json index 31c726f9..46cdc228 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.json +++ b/ui/src/i18n/locales/zh/nls.workflow.json @@ -3,6 +3,8 @@ "workflow.nodata": "暂无工作流,请先新建工作流", + "workflow.search.placeholder": "按工作流名称搜索……", + "workflow.action.create": "新建工作流", "workflow.action.edit": "编辑工作流", "workflow.action.delete": "删除工作流", diff --git a/ui/src/pages/accesses/AccessList.tsx b/ui/src/pages/accesses/AccessList.tsx index 1f1d1232..66cdeab8 100644 --- a/ui/src/pages/accesses/AccessList.tsx +++ b/ui/src/pages/accesses/AccessList.tsx @@ -1,14 +1,16 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useSearchParams } from "react-router-dom"; import { DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon, PlusOutlined as PlusOutlinedIcon, + ReloadOutlined as ReloadOutlinedIcon, SnippetsOutlined as SnippetsOutlinedIcon, } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; -import { Avatar, Button, Empty, Modal, Space, Table, type TableProps, Tooltip, Typography, notification } from "antd"; +import { Avatar, Button, Card, Empty, Flex, Input, Modal, Space, Table, type TableProps, Tooltip, Typography, notification } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; @@ -20,6 +22,8 @@ import { useAccessesStore } from "@/stores/access"; import { getErrMsg } from "@/utils/error"; const AccessList = () => { + const [searchParams] = useSearchParams(); + const { t } = useTranslation(); const [modalApi, ModelContextHolder] = Modal.useModal(); @@ -116,6 +120,12 @@ const AccessList = () => { const [tableData, setTableData] = useState([]); const [tableTotal, setTableTotal] = useState(0); + const [filters, setFilters] = useState>(() => { + return { + keyword: searchParams.get("keyword"), + }; + }); + const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); @@ -134,14 +144,21 @@ const AccessList = () => { () => { const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; - const items = accesses.slice(startIndex, endIndex); + const list = accesses.filter((e) => { + const keyword = (filters["keyword"] as string | undefined)?.trim(); + if (keyword) { + return e.name.includes(keyword); + } + + return true; + }); return Promise.resolve({ - items, - totalItems: accesses.length, + items: list.slice(startIndex, endIndex), + totalItems: list.length, }); }, { - refreshDeps: [accesses, page, pageSize], + refreshDeps: [accesses, filters, page, pageSize], onSuccess: (res) => { setTableData(res.items); setTableTotal(res.totalItems); @@ -149,6 +166,16 @@ const AccessList = () => { } ); + const handleSearch = (value: string) => { + setFilters((prev) => ({ ...prev, keyword: value })); + }; + + const handleReloadClick = () => { + if (loading) return; + + fetchAccesses(); + }; + const handleDeleteClick = async (data: AccessModel) => { modalApi.confirm({ title: t("access.action.delete"), @@ -186,30 +213,43 @@ const AccessList = () => { ]} /> - - columns={tableColumns} - dataSource={tableData} - loading={!loadedAtOnce || loading} - locale={{ - emptyText: , - }} - pagination={{ - current: page, - pageSize: pageSize, - total: tableTotal, - showSizeChanger: true, - onChange: (page: number, pageSize: number) => { - setPage(page); - setPageSize(pageSize); - }, - onShowSizeChange: (page: number, pageSize: number) => { - setPage(page); - setPageSize(pageSize); - }, - }} - rowKey={(record) => record.id} - scroll={{ x: "max(100%, 960px)" }} - /> + +
+ +
+ +
+
+
+
+
+ + + columns={tableColumns} + dataSource={tableData} + loading={!loadedAtOnce || loading} + locale={{ + emptyText: , + }} + pagination={{ + current: page, + pageSize: pageSize, + total: tableTotal, + showSizeChanger: true, + onChange: (page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); + }, + onShowSizeChange: (page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); + }, + }} + rowKey={(record) => record.id} + scroll={{ x: "max(100%, 960px)" }} + /> +
); }; diff --git a/ui/src/pages/certificates/CertificateList.tsx b/ui/src/pages/certificates/CertificateList.tsx index 2198fc83..265f0185 100644 --- a/ui/src/pages/certificates/CertificateList.tsx +++ b/ui/src/pages/certificates/CertificateList.tsx @@ -1,10 +1,28 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; -import { DeleteOutlined as DeleteOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; +import { DeleteOutlined as DeleteOutlinedIcon, ReloadOutlined as ReloadOutlinedIcon, SelectOutlined as SelectOutlinedIcon } from "@ant-design/icons"; import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; -import { Button, Divider, Empty, Menu, type MenuProps, Modal, Radio, Space, Table, type TableProps, Tooltip, Typography, notification, theme } from "antd"; +import { + Button, + Card, + Divider, + Empty, + Flex, + Input, + Menu, + type MenuProps, + Modal, + Radio, + Space, + Table, + type TableProps, + Tooltip, + Typography, + notification, + theme, +} from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; @@ -191,6 +209,7 @@ const CertificateList = () => { const [filters, setFilters] = useState>(() => { return { + keyword: searchParams.get("keyword"), state: searchParams.get("state"), }; }); @@ -205,9 +224,10 @@ const CertificateList = () => { } = useRequest( () => { return listCertificate({ + keyword: filters["keyword"] as string, + state: filters["state"] as ListCertificateRequest["state"], page: page, perPage: pageSize, - state: filters["state"] as ListCertificateRequest["state"], }); }, { @@ -229,6 +249,16 @@ const CertificateList = () => { } ); + const handleSearch = (value: string) => { + setFilters((prev) => ({ ...prev, keyword: value.trim() })); + }; + + const handleReloadClick = () => { + if (loading) return; + + refreshData(); + }; + const handleDeleteClick = (certificate: CertificateModel) => { modalApi.confirm({ title: t("certificate.action.delete"), @@ -255,30 +285,43 @@ const CertificateList = () => { - - columns={tableColumns} - dataSource={tableData} - loading={loading} - locale={{ - emptyText: , - }} - pagination={{ - current: page, - pageSize: pageSize, - total: tableTotal, - showSizeChanger: true, - onChange: (page: number, pageSize: number) => { - setPage(page); - setPageSize(pageSize); - }, - onShowSizeChange: (page: number, pageSize: number) => { - setPage(page); - setPageSize(pageSize); - }, - }} - rowKey={(record) => record.id} - scroll={{ x: "max(100%, 960px)" }} - /> + +
+ +
+ +
+
+
+
+
+ + + columns={tableColumns} + dataSource={tableData} + loading={loading} + locale={{ + emptyText: , + }} + pagination={{ + current: page, + pageSize: pageSize, + total: tableTotal, + showSizeChanger: true, + onChange: (page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); + }, + onShowSizeChange: (page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); + }, + }} + rowKey={(record) => record.id} + scroll={{ x: "max(100%, 960px)" }} + /> +
); }; diff --git a/ui/src/pages/workflows/WorkflowList.tsx b/ui/src/pages/workflows/WorkflowList.tsx index d0536d6e..18a8b577 100644 --- a/ui/src/pages/workflows/WorkflowList.tsx +++ b/ui/src/pages/workflows/WorkflowList.tsx @@ -8,6 +8,7 @@ import { DeleteOutlined as DeleteOutlinedIcon, EditOutlined as EditOutlinedIcon, PlusOutlined as PlusOutlinedIcon, + ReloadOutlined as ReloadOutlinedIcon, StopOutlined as StopOutlinedIcon, SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; @@ -16,8 +17,11 @@ import { PageHeader } from "@ant-design/pro-components"; import { useRequest } from "ahooks"; import { Button, + Card, Divider, Empty, + Flex, + Input, Menu, type MenuProps, Modal, @@ -235,6 +239,7 @@ const WorkflowList = () => { const [filters, setFilters] = useState>(() => { return { + keyword: searchParams.get("keyword"), state: searchParams.get("state"), }; }); @@ -249,9 +254,10 @@ const WorkflowList = () => { } = useRequest( () => { return listWorkflow({ + keyword: filters["keyword"] as string, + enabled: (filters["state"] as string) === "enabled" ? true : (filters["state"] as string) === "disabled" ? false : undefined, page: page, perPage: pageSize, - enabled: (filters["state"] as string) === "enabled" ? true : (filters["state"] as string) === "disabled" ? false : undefined, }); }, { @@ -273,10 +279,20 @@ const WorkflowList = () => { } ); + const handleSearch = (value: string) => { + setFilters((prev) => ({ ...prev, keyword: value.trim() })); + }; + const handleCreateClick = () => { navigate("/workflows/new"); }; + const handleReloadClick = () => { + if (loading) return; + + refreshData(); + }; + const handleEnabledChange = async (workflow: WorkflowModel) => { try { if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) { @@ -345,30 +361,43 @@ const WorkflowList = () => { ]} /> - - columns={tableColumns} - dataSource={tableData} - loading={loading} - locale={{ - emptyText: , - }} - pagination={{ - current: page, - pageSize: pageSize, - total: tableTotal, - showSizeChanger: true, - onChange: (page: number, pageSize: number) => { - setPage(page); - setPageSize(pageSize); - }, - onShowSizeChange: (page: number, pageSize: number) => { - setPage(page); - setPageSize(pageSize); - }, - }} - rowKey={(record) => record.id} - scroll={{ x: "max(100%, 960px)" }} - /> + +
+ +
+ +
+
+
+
+
+ + + columns={tableColumns} + dataSource={tableData} + loading={loading} + locale={{ + emptyText: , + }} + pagination={{ + current: page, + pageSize: pageSize, + total: tableTotal, + showSizeChanger: true, + onChange: (page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); + }, + onShowSizeChange: (page: number, pageSize: number) => { + setPage(page); + setPageSize(pageSize); + }, + }} + rowKey={(record) => record.id} + scroll={{ x: "max(100%, 960px)" }} + /> +
); }; diff --git a/ui/src/repository/access.ts b/ui/src/repository/access.ts index 4b21d5da..21127dcf 100644 --- a/ui/src/repository/access.ts +++ b/ui/src/repository/access.ts @@ -4,11 +4,16 @@ import { type AccessModel } from "@/domain/access"; import { COLLECTION_NAME_ACCESS, getPocketBase } from "./_pocketbase"; export const list = async () => { - return await getPocketBase().collection(COLLECTION_NAME_ACCESS).getFullList({ + const list = await getPocketBase().collection(COLLECTION_NAME_ACCESS).getFullList({ + batch: 65535, filter: "deleted=null", sort: "-created", requestKey: null, }); + return { + totalItems: list.length, + items: list, + }; }; export const save = async (record: MaybeModelRecord) => { diff --git a/ui/src/repository/certificate.ts b/ui/src/repository/certificate.ts index 61de9c51..b6b8d55e 100644 --- a/ui/src/repository/certificate.ts +++ b/ui/src/repository/certificate.ts @@ -1,55 +1,51 @@ import dayjs from "dayjs"; -import { type RecordListOptions } from "pocketbase"; import { type CertificateModel } from "@/domain/certificate"; import { COLLECTION_NAME_CERTIFICATE, getPocketBase } from "./_pocketbase"; export type ListCertificateRequest = { + keyword?: string; + state?: "expireSoon" | "expired"; page?: number; perPage?: number; - state?: "expireSoon" | "expired"; }; export const list = async (request: ListCertificateRequest) => { const pb = getPocketBase(); - const page = request.page || 1; - const perPage = request.perPage || 10; - - const options: RecordListOptions = { - expand: "workflowId", - filter: "deleted=null", - sort: "-created", - requestKey: null, - }; - + const filters: string[] = ["deleted=null"]; + if (request.keyword) { + filters.push(pb.filter("(subjectAltNames~{:keyword} || serialNumber={:keyword})", { keyword: request.keyword })); + } if (request.state === "expireSoon") { - options.filter = pb.filter("expireAt<{:expiredAt} && deleted=null", { - expiredAt: dayjs().add(20, "d").toDate(), - }); + filters.push(pb.filter("expireAt<{:expiredAt}", { expiredAt: dayjs().add(20, "d").toDate() })); } else if (request.state === "expired") { - options.filter = pb.filter("expireAt<={:expiredAt} && deleted=null", { - expiredAt: new Date(), - }); + filters.push(pb.filter("expireAt<={:expiredAt}", { expiredAt: new Date() })); } - return pb.collection(COLLECTION_NAME_CERTIFICATE).getList(page, perPage, options); + 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", + requestKey: null, + }); }; export const listByWorkflowRunId = async (workflowRunId: string) => { const pb = getPocketBase(); - const options: RecordListOptions = { - filter: pb.filter("workflowRunId={:workflowRunId}", { - workflowRunId: workflowRunId, - }), + const list = await pb.collection(COLLECTION_NAME_CERTIFICATE).getFullList({ + batch: 65535, + filter: pb.filter("workflowRunId={:workflowRunId}", { workflowRunId: workflowRunId }), sort: "-created", requestKey: null, - }; - const items = await pb.collection(COLLECTION_NAME_CERTIFICATE).getFullList(options); + }); + return { - totalItems: items.length, - items: items, + totalItems: list.length, + items: list, }; }; diff --git a/ui/src/repository/workflow.ts b/ui/src/repository/workflow.ts index 9b0f6ef5..0b35a5e2 100644 --- a/ui/src/repository/workflow.ts +++ b/ui/src/repository/workflow.ts @@ -1,30 +1,33 @@ -import { type RecordListOptions, type RecordSubscription } from "pocketbase"; +import { type RecordSubscription } from "pocketbase"; import { type WorkflowModel } from "@/domain/workflow"; import { COLLECTION_NAME_WORKFLOW, getPocketBase } from "./_pocketbase"; export type ListWorkflowRequest = { + keyword?: string; + enabled?: boolean; page?: number; perPage?: number; - enabled?: boolean; }; export const list = async (request: ListWorkflowRequest) => { const pb = getPocketBase(); - const page = request.page || 1; - const perPage = request.perPage || 10; - - const options: RecordListOptions = { - sort: "-created", - requestKey: null, - }; - + const filters: string[] = []; + if (request.keyword) { + filters.push(pb.filter("name~{:keyword}", { keyword: request.keyword })); + } if (request.enabled != null) { - options.filter = pb.filter("enabled={:enabled}", { enabled: request.enabled }); + filters.push(pb.filter("enabled={:enabled}", { enabled: request.enabled })); } - return await pb.collection(COLLECTION_NAME_WORKFLOW).getList(page, perPage, options); + const page = request.page || 1; + const perPage = request.perPage || 10; + return await pb.collection(COLLECTION_NAME_WORKFLOW).getList(page, perPage, { + filter: filters.join(" && "), + sort: "-created", + requestKey: null, + }); }; export const get = async (id: string) => { diff --git a/ui/src/repository/workflowRun.ts b/ui/src/repository/workflowRun.ts index 4ede9a6f..51038f18 100644 --- a/ui/src/repository/workflowRun.ts +++ b/ui/src/repository/workflowRun.ts @@ -12,24 +12,21 @@ export type ListWorkflowRunsRequest = { }; export const list = async (request: ListWorkflowRunsRequest) => { - const page = request.page || 1; - const perPage = request.perPage || 10; + const pb = getPocketBase(); - let filter = ""; - const params: Record = {}; + const filters: string[] = []; if (request.workflowId) { - filter = `workflowId={:workflowId}`; - params.workflowId = request.workflowId; + filters.push(pb.filter("workflowId={:workflowId}", { workflowId: request.workflowId })); } - return await getPocketBase() - .collection(COLLECTION_NAME_WORKFLOW_RUN) - .getList(page, perPage, { - filter: getPocketBase().filter(filter, params), - sort: "-created", - requestKey: null, - expand: request.expand ? "workflowId" : undefined, - }); + const page = request.page || 1; + const perPage = request.perPage || 10; + return await pb.collection(COLLECTION_NAME_WORKFLOW_RUN).getList(page, perPage, { + filter: filters.join(" && "), + sort: "-created", + requestKey: null, + expand: request.expand ? "workflowId" : undefined, + }); }; export const remove = async (record: MaybeModelRecordWithId) => { diff --git a/ui/src/stores/access/index.ts b/ui/src/stores/access/index.ts index 661dbedc..61601978 100644 --- a/ui/src/stores/access/index.ts +++ b/ui/src/stores/access/index.ts @@ -24,7 +24,7 @@ export const useAccessesStore = create((set) => { loadedAtOnce: false, fetchAccesses: async () => { - fetcher ??= listAccess(); + fetcher ??= listAccess().then((res) => res.items); try { set({ loading: true });