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 9ccfba3e9 fix(web): tolerate incomplete certificate metadata (#2560)
9ccfba3e9 is described below

commit 9ccfba3e9d5aabcc6f4c30aaf9d859f2a405c6b1
Author: yyqdbngt <[email protected]>
AuthorDate: Tue Aug 25 17:34:47 2026 +0800

    fix(web): tolerate incomplete certificate metadata (#2560)
---
 web/src/api/cluster.ts                             | 12 +++++------
 .../pages/cluster/__tests__/K8sCertsPage.test.tsx  | 24 ++++++++++++++++++++++
 web/src/pages/cluster/certs.tsx                    | 21 ++++++++++---------
 web/src/services/clusterService.ts                 | 10 +++++----
 4 files changed, 47 insertions(+), 20 deletions(-)

diff --git a/web/src/api/cluster.ts b/web/src/api/cluster.ts
index fb9869f89..44c6f9d38 100644
--- a/web/src/api/cluster.ts
+++ b/web/src/api/cluster.ts
@@ -136,13 +136,13 @@ export interface K8sCertInfo {
   id: number;
   k8sId: string;
   cluster: string;
-  type: string;
-  issuer: string;
-  notBefore: string;
-  notAfter: string;
-  status: string;
+  type: string | null;
+  issuer: string | null;
+  notBefore: string | null;
+  notAfter: string | null;
+  status: string | null;
   daysRemaining: number;
-  san: string[];
+  san: string[] | null;
   certPem?: string;
   keyPem?: string;
 }
diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx 
b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
index 402719bf6..dd52f7815 100644
--- a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
@@ -42,6 +42,18 @@ const certs: K8sCertInfo[] = [
     daysRemaining: 365,
     san: ['broker.prod.example.com'],
   },
+  {
+    id: 3,
+    k8sId: 'metadata-only-tls',
+    cluster: 'legacy-cluster',
+    type: null,
+    issuer: null,
+    notBefore: null,
+    notAfter: null,
+    status: null,
+    daysRemaining: 0,
+    san: null,
+  },
   {
     id: 2,
     k8sId: 'rocketmq-staging-tls',
@@ -97,6 +109,18 @@ describe('K8sCertsPage', () => {
     expect(screen.queryByText('SAN')).not.toBeInTheDocument();
   });
 
+  it('renders and sorts incomplete certificate metadata safely', async () => {
+    const user = userEvent.setup();
+    renderPage();
+
+    await screen.findByText('metadata-only-tls');
+    await user.click(screen.getByRole('columnheader', { name: /签发者/ }));
+    await user.click(screen.getByRole('columnheader', { name: /到期时间/ }));
+
+    expect(screen.getByText('metadata-only-tls')).toBeInTheDocument();
+    expect(screen.getAllByText('-').length).toBeGreaterThan(0);
+  });
+
   it('explains that certificate records are Studio-local metadata', async () 
=> {
     renderPage();
 
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index 1f6397f23..1dd7d4b72 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -156,14 +156,14 @@ const K8sCertsPage = () => {
       dataIndex: 'type',
       key: 'type',
       width: 110,
-      sorter: (a, b) => a.type.localeCompare(b.type),
-      render: (type: string) => {
+      sorter: (a, b) => (a.type ?? '').localeCompare(b.type ?? ''),
+      render: (type: string | null) => {
         const colorMap: Record<string, string> = {
           TLS: 'blue',
           mTLS: 'purple',
           ServiceAccount: 'orange',
         };
-        return <Tag color={colorMap[type] ?? 'default'}>{type}</Tag>;
+        return type ? <Tag color={colorMap[type] ?? 'default'}>{type}</Tag> : 
'-';
       },
     },
     {
@@ -171,7 +171,8 @@ const K8sCertsPage = () => {
       dataIndex: 'issuer',
       key: 'issuer',
       width: 180,
-      sorter: (a, b) => a.issuer.localeCompare(b.issuer),
+      sorter: (a, b) => (a.issuer ?? '').localeCompare(b.issuer ?? ''),
+      render: (issuer: string | null) => issuer || '-',
       ellipsis: true,
     },
     {
@@ -179,8 +180,8 @@ const K8sCertsPage = () => {
       dataIndex: 'notAfter',
       key: 'notAfter',
       width: 170,
-      sorter: (a, b) => new Date(a.notAfter).getTime() - new 
Date(b.notAfter).getTime(),
-      render: (iso: string) => (
+      sorter: (a, b) => (Date.parse(a.notAfter ?? '') || 0) - 
(Date.parse(b.notAfter ?? '') || 0),
+      render: (iso: string | null) => (
         <Text type="secondary" style={{ fontSize: 14 }}>
           {formatDateTime(iso)}
         </Text>
@@ -208,15 +209,15 @@ const K8sCertsPage = () => {
       dataIndex: 'status',
       key: 'status',
       width: 100,
-      sorter: (a, b) => a.status.localeCompare(b.status),
-      render: (status: string) => {
+      sorter: (a, b) => (a.status ?? '').localeCompare(b.status ?? ''),
+      render: (status: string | null) => {
         const map: Record<string, { color: string; label: string }> = {
           valid: { color: 'green', label: '有效' },
           expiring: { color: 'orange', label: '即将过期' },
           expired: { color: 'red', label: '已过期' },
         };
-        const cfg = map[status] ?? { color: 'default', label: status };
-        return <Tag color={cfg.color}>{cfg.label}</Tag>;
+        const cfg = status ? map[status] ?? { color: 'default', label: status 
} : null;
+        return cfg ? <Tag color={cfg.color}>{cfg.label}</Tag> : '-';
       },
     },
     {
diff --git a/web/src/services/clusterService.ts 
b/web/src/services/clusterService.ts
index c147c4240..8fc5f5c7d 100644
--- a/web/src/services/clusterService.ts
+++ b/web/src/services/clusterService.ts
@@ -142,7 +142,9 @@ export async function getNameServerConfigDiff(
 }
 
 export async function listK8sCerts(): Promise<K8sCertInfo[]> {
-  if (isMockMode()) return mockCertStore.map((cert) => ({ ...cert, san: 
[...cert.san] }));
+  if (isMockMode()) {
+    return mockCertStore.map((cert) => ({ ...cert, san: cert.san ? 
[...cert.san] : cert.san }));
+  }
   return clusterApi.listK8sCerts();
 }
 
@@ -164,7 +166,7 @@ export async function createK8sCert(data: 
Partial<K8sCertInfo>): Promise<K8sCert
       san: [...(data.san ?? [])],
     };
     mockCertStore.push(cert);
-    return { ...cert, san: [...cert.san] };
+    return { ...cert, san: cert.san ? [...cert.san] : cert.san };
   }
   return clusterApi.createK8sCert(data);
 }
@@ -174,7 +176,7 @@ export async function updateK8sCert(data: 
Partial<K8sCertInfo>): Promise<K8sCert
     const existing = mockCertStore.find((cert) => cert.id === data.id);
     if (!existing) throw new Error(`Certificate not found: ${data.id}`);
     Object.assign(existing, data, { san: data.san ? [...data.san] : 
existing.san });
-    return { ...existing, san: [...existing.san] };
+    return { ...existing, san: existing.san ? [...existing.san] : existing.san 
};
   }
   return clusterApi.updateK8sCert(data);
 }
@@ -192,7 +194,7 @@ export async function renewK8sCert(id: number): 
Promise<K8sCertInfo> {
       status: 'valid',
       daysRemaining: Math.round((notAfter.getTime() - now.getTime()) / (24 * 
60 * 60 * 1000)),
     });
-    return { ...existing, san: [...existing.san] };
+    return { ...existing, san: existing.san ? [...existing.san] : existing.san 
};
   }
   return clusterApi.renewK8sCert(id);
 }

Reply via email to