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 本地配置"