You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/10/15 10:16:55 UTC
(rocketmq-dashboard) branch rocketmq-studio updated: fix(cluster): report a failed certificate-list load instead of an empty inventory (#6409)
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 f4de8e8b0 fix(cluster): report a failed certificate-list load instead of an empty inventory (#6409)
f4de8e8b0 is described below
commit f4de8e8b0107b01ca2ce592e4794e1259863cac4
Author: bai mengxuan <20...@qq.com>
AuthorDate: Thu Oct 15 18:16:49 2026 +0800
fix(cluster): report a failed certificate-list load instead of an empty inventory (#6409)
The K8s certificate page answered a failed listK8sCerts() with a transient toast
only: the rows stayed empty, the table rendered its empty state and the header
asserted "共 0 个证书" for a read that never returned - so an operator auditing
certificate expiry saw a cluster without certificates. The effect also ran once per
mount, so the only recovery was a full page reload.
The failure is now state on the page: the previous rows are kept, the header reports
the count as unavailable instead of a measured zero, and an error banner offers a
retry that re-runs the load (the shape the Grafana dashboard list already uses).
Tests: K8sCertsPage.test.tsx gains a case that rejects the load, asserts the banner,
the unavailable header and a retry that recovers the rows; it fails on the previous
code ("Unable to find an element with the text: 获取数据失败") and the suite is 12/12.
---
.../pages/cluster/__tests__/K8sCertsPage.test.tsx | 18 +++++++++
web/src/pages/cluster/certs.tsx | 43 +++++++++++++++++++---
2 files changed, 56 insertions(+), 5 deletions(-)
diff --git a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
index e932ec10d..a8032ca69 100644
--- a/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/K8sCertsPage.test.tsx
@@ -250,4 +250,22 @@ describe('K8sCertsPage', () => {
expect(await screen.findByText('请求失败,请稍后重试')).toBeInTheDocument();
});
+
+ it('reports a failed certificate load with a retry instead of an empty inventory', async () => {
+ vi.mocked(listK8sCerts).mockRejectedValueOnce(new Error('certificate store offline'));
+ const user = userEvent.setup();
+
+ renderPage();
+
+ expect(await screen.findByText('获取数据失败')).toBeInTheDocument();
+ // A failed read must not be presented as a measured zero.
+ expect(screen.getByText('不可用')).toBeInTheDocument();
+ expect(screen.queryByText('共 0 个证书')).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole('button', { name: /重试/ }));
+
+ await screen.findByText('rocketmq-prod-tls');
+ await waitFor(() => expect(screen.queryByText('获取数据失败')).not.toBeInTheDocument());
+ expect(screen.getByText('共 3 个证书')).toBeInTheDocument();
+ });
});
diff --git a/web/src/pages/cluster/certs.tsx b/web/src/pages/cluster/certs.tsx
index 412245b2f..790500dc8 100644
--- a/web/src/pages/cluster/certs.tsx
+++ b/web/src/pages/cluster/certs.tsx
@@ -29,9 +29,10 @@ import {
Modal,
Form,
Popconfirm,
+ Alert,
message,
} from 'antd';
-import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
+import { PlusOutlined, DeleteOutlined, ReloadOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import PageHeader from '../../components/PageHeader';
import InfoBanner from '../../components/InfoBanner';
@@ -40,6 +41,7 @@ import { listK8sCerts, createK8sCert, deleteK8sCert } from '../../services/clust
import { describeThrownMessage } from '../../utils/apiError';
import { formatUtcDateTime } from '../../utils/format';
import { tableScrollX } from '../../utils/table';
+import { useLang } from '../../i18n/LangContext';
const { Text } = Typography;
@@ -54,8 +56,11 @@ interface CreateCertFormValues {
}
const K8sCertsPage = () => {
+ const { t } = useLang();
const [certs, setCerts] = useState<K8sCertInfo[]>([]);
const [loading, setLoading] = useState(true);
+ const [loadFailed, setLoadFailed] = useState(false);
+ const [reloadNonce, setReloadNonce] = useState(0);
const [certSearch, setCertSearch] = useState('');
const [certTypeFilter, setCertTypeFilter] = useState<string>('');
const [createModalOpen, setCreateModalOpen] = useState(false);
@@ -67,10 +72,18 @@ const K8sCertsPage = () => {
let active = true;
listK8sCerts()
.then((data) => {
- if (active) setCerts(data);
+ if (active) {
+ setCerts(data);
+ setLoadFailed(false);
+ }
})
.catch((error: unknown) => {
- if (active) message.error(describeThrownMessage(error) || DEFAULT_REQUEST_ERROR);
+ // Keeping the previous rows and marking the failure: clearing the list made a failed read
+ // look like a cluster without certificates, and nothing offered a retry.
+ if (active) {
+ message.error(describeThrownMessage(error) || DEFAULT_REQUEST_ERROR);
+ setLoadFailed(true);
+ }
})
.finally(() => {
if (active) setLoading(false);
@@ -78,7 +91,7 @@ const K8sCertsPage = () => {
return () => {
active = false;
};
- }, []);
+ }, [reloadNonce]);
const normalizedCertSearch = certSearch.trim().toLowerCase();
const filteredCerts = certs.filter((cert) => {
@@ -250,7 +263,27 @@ const K8sCertsPage = () => {
return (
<div style={{ padding: 24 }}>
- <PageHeader title="K8s 证书配置" subtitle={`共 ${filteredCerts.length} 个证书`} />
+ <PageHeader
+ title="K8s 证书配置"
+ subtitle={loadFailed ? t('common.unavailable') : `共 ${filteredCerts.length} 个证书`}
+ />
+ {loadFailed && (
+ <Alert
+ showIcon
+ type="error"
+ message={t('common.fetchDataFailed')}
+ action={
+ <Button
+ size="small"
+ icon={<ReloadOutlined />}
+ onClick={() => setReloadNonce((nonce) => nonce + 1)}
+ >
+ {t('common.retry')}
+ </Button>
+ }
+ style={{ marginBottom: 12 }}
+ />
+ )}
<InfoBanner
data-testid="k8s-cert-local-metadata-notice"
title="当前证书记录仅保存为 Studio 本地配置"