This is an automated email from the ASF dual-hosted git repository.

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 06f2d105 feat: improve K8s certificate visibility and filtering (#713)
06f2d105 is described below

commit 06f2d105e2524511f69043114026e1e469479c52
Author: yx9o <[email protected]>
AuthorDate: Mon Aug 3 11:11:44 2026 +0800

    feat: improve K8s certificate visibility and filtering (#713)
---
 .../pages/cluster/__tests__/K8sCertsPage.test.tsx  | 67 ++++++++++++++++++++--
 web/src/pages/cluster/certs.tsx                    | 50 ++++++++++++++--
 2 files changed, 105 insertions(+), 12 deletions(-)

diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx 
b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
index 0e7381d6..bd6eccb2 100644
--- a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
@@ -16,7 +16,7 @@
  */
 
 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
-import { render, screen } from '@testing-library/react';
+import { fireEvent, render, screen } from '@testing-library/react';
 import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import type { K8sCertInfo } from '../../../api/cluster';
@@ -48,7 +48,7 @@ const certs: K8sCertInfo[] = [
   {
     id: 'cert-staging',
     name: 'rocketmq-staging-tls',
-    namespace: 'rocketmq',
+    namespace: 'platform',
     cluster: 'staging-cluster',
     type: 'TLS',
     issuer: 'kubernetes-ca',
@@ -56,7 +56,8 @@ const certs: K8sCertInfo[] = [
     notAfter: '2027-01-01T00:00:00Z',
     status: 'valid',
     daysRemaining: 365,
-    san: ['broker.staging.example.com'],
+    // The backend returns null when SAN is omitted.
+    san: null as unknown as string[],
   },
 ];
 
@@ -81,16 +82,70 @@ describe('K8sCertsPage', () => {
     vi.mocked(listK8sCerts).mockResolvedValue(certs);
   });
 
-  it('trims certificate search text before filtering', async () => {
-    const user = userEvent.setup();
+  const renderPage = () =>
     render(
       <App>
         <K8sCertsPage />
       </App>,
     );
 
+  it('displays certificate namespace and SAN metadata', async () => {
+    renderPage();
+
+    await screen.findByText('rocketmq-prod-tls');
+
+    expect(screen.getByText('rocketmq')).toBeInTheDocument();
+    expect(screen.getByText('platform')).toBeInTheDocument();
+    expect(screen.getByText('broker.prod.example.com')).toBeInTheDocument();
+    expect(screen.getByText('-')).toBeInTheDocument();
+  });
+
+  it.each([
+    ['platform', 'rocketmq-staging-tls', 'rocketmq-prod-tls'],
+    ['broker.prod.example.com', 'rocketmq-prod-tls', 'rocketmq-staging-tls'],
+  ])('searches certificate metadata by %s', async (query, expected, hidden) => 
{
+    const user = userEvent.setup();
+    renderPage();
+
     await screen.findByText('rocketmq-prod-tls');
-    await user.type(screen.getByPlaceholderText('搜索证书名称或集群'), '  prod-cluster  
{enter}');
+    await user.type(
+      screen.getByPlaceholderText('搜索证书名称、集群、命名空间或 SAN'),
+      `${query}{enter}`,
+    );
+
+    expect(screen.getByText(expected)).toBeInTheDocument();
+    expect(screen.queryByText(hidden)).not.toBeInTheDocument();
+  });
+
+  it('filters certificates by namespace', async () => {
+    const user = userEvent.setup();
+    renderPage();
+
+    await screen.findByText('rocketmq-prod-tls');
+    const namespaceFilter = screen.getByRole('combobox', { name: '按命名空间筛选' });
+    const selector = namespaceFilter
+      .closest('.ant-select')
+      ?.querySelector<HTMLElement>('.ant-select-selector');
+
+    expect(selector).not.toBeNull();
+    fireEvent.mouseDown(selector!);
+    await user.click(
+      await screen.findByText('platform', { selector: 
'.ant-select-item-option-content' }),
+    );
+
+    expect(screen.getByText('rocketmq-staging-tls')).toBeInTheDocument();
+    expect(screen.queryByText('rocketmq-prod-tls')).not.toBeInTheDocument();
+  });
+
+  it('trims certificate search text before filtering', async () => {
+    const user = userEvent.setup();
+    renderPage();
+
+    await screen.findByText('rocketmq-prod-tls');
+    await user.type(
+      screen.getByPlaceholderText('搜索证书名称、集群、命名空间或 SAN'),
+      '  prod-cluster  {enter}',
+    );
 
     expect(screen.getByText('rocketmq-prod-tls')).toBeInTheDocument();
     expect(screen.queryByText('rocketmq-staging-tls')).not.toBeInTheDocument();
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index a9408b13..e0b95bdc 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -60,6 +60,7 @@ const K8sCertsPage = () => {
   const [renewingId, setRenewingId] = useState<string | null>(null);
   const [certSearch, setCertSearch] = useState('');
   const [certTypeFilter, setCertTypeFilter] = useState<string>('');
+  const [certNamespaceFilter, setCertNamespaceFilter] = useState<string>('');
   const [editModalOpen, setEditModalOpen] = useState(false);
   const [editingCert, setEditingCert] = useState<K8sCertInfo | null>(null);
   const [editForm] = Form.useForm();
@@ -130,13 +131,18 @@ const K8sCertsPage = () => {
   };
 
   const normalizedCertSearch = certSearch.trim().toLowerCase();
+  const namespaceOptions = Array.from(new Set(certs.map((cert) => 
cert.namespace)))
+    .sort((a, b) => a.localeCompare(b))
+    .map((namespace) => ({ value: namespace, label: namespace }));
   const filteredCerts = certs.filter((cert) => {
     const matchSearch =
       !normalizedCertSearch ||
-      cert.name.toLowerCase().includes(normalizedCertSearch) ||
-      cert.cluster.toLowerCase().includes(normalizedCertSearch);
+      [cert.name, cert.cluster, cert.namespace, ...(cert.san ?? 
[])].some((value) =>
+        value.toLowerCase().includes(normalizedCertSearch),
+      );
     const matchType = !certTypeFilter || cert.type === certTypeFilter;
-    return matchSearch && matchType;
+    const matchNamespace = !certNamespaceFilter || cert.namespace === 
certNamespaceFilter;
+    return matchSearch && matchType && matchNamespace;
   });
 
   const renewCert = async (cert: K8sCertInfo) => {
@@ -172,6 +178,30 @@ const K8sCertsPage = () => {
         <Text style={{ fontFamily: 'monospace', fontSize: 13 }}>{name}</Text>
       ),
     },
+    {
+      title: '命名空间',
+      dataIndex: 'namespace',
+      key: 'namespace',
+      width: 150,
+      sorter: (a, b) => a.namespace.localeCompare(b.namespace),
+      render: (namespace: string) => <Text code>{namespace}</Text>,
+    },
+    {
+      title: 'SAN',
+      dataIndex: 'san',
+      key: 'san',
+      width: 260,
+      render: (san: string[] | null) =>
+        san?.length ? (
+          <Flex wrap gap={4}>
+            {san.map((value) => (
+              <Tag key={value}>{value}</Tag>
+            ))}
+          </Flex>
+        ) : (
+          <Text type="secondary">-</Text>
+        ),
+    },
     {
       title: '类型',
       dataIndex: 'type',
@@ -257,7 +287,7 @@ const K8sCertsPage = () => {
                 issuer: record.issuer,
                 namespace: record.namespace,
                 cluster: record.cluster,
-                san: record.san.join(', '),
+                san: record.san?.join(', ') ?? '',
               });
               setEditModalOpen(true);
             }}
@@ -325,11 +355,18 @@ const K8sCertsPage = () => {
       <Flex justify="space-between" style={{ marginBottom: 16 }}>
         <Space>
           <Input.Search
-            placeholder="搜索证书名称或集群"
+            placeholder="搜索证书名称、集群、命名空间或 SAN"
             allowClear
             onSearch={setCertSearch}
             onChange={(e) => !e.target.value && setCertSearch('')}
-            style={{ width: 240 }}
+            style={{ width: 320 }}
+          />
+          <Select
+            aria-label="按命名空间筛选"
+            value={certNamespaceFilter}
+            onChange={setCertNamespaceFilter}
+            style={{ width: 180 }}
+            options={[{ value: '', label: '全部命名空间' }, ...namespaceOptions]}
           />
           <Select
             value={certTypeFilter}
@@ -352,6 +389,7 @@ const K8sCertsPage = () => {
           loading={loading}
           pagination={{ pageSize: 20 }}
           size="small"
+          scroll={{ x: 1750 }}
         />
       </Card>
 

Reply via email to