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);
}