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 00ed28751 fix(web): stop charting an unmapped client language as null 
(#4850)
00ed28751 is described below

commit 00ed28751880ba65aafcc64cc9e1b0c766e1a23d
Author: Wang1rrr <[email protected]>
AuthorDate: Thu Sep 24 10:47:17 2026 +0800

    fix(web): stop charting an unmapped client language as null (#4850)
    
    `ClientConnectionVO.language` is nullable on the server (`mapLanguage` 
returns null for every `LanguageCode` with no `ClientLanguage` member, and the 
VO is serialised without NON_NULL) while `api/connections.ts` declared it as a 
required string, so `strictNullChecks` could not flag any consumer and three of 
them rendered the gap: the language distribution interpolated null into its 
bucket label, and both the Language column and the detail drawer looked the 
value up in `languageConfig` [...]
---
 web/src/api/connections.ts                         |  2 +-
 web/src/i18n/translations.ts                       |  1 +
 .../pages/cluster/__tests__/ClientsPage.test.tsx   | 21 +++++++++++++
 web/src/pages/cluster/clients.tsx                  | 36 ++++++++++++++++------
 4 files changed, 49 insertions(+), 11 deletions(-)

diff --git a/web/src/api/connections.ts b/web/src/api/connections.ts
index a47d2de8a..38e163604 100644
--- a/web/src/api/connections.ts
+++ b/web/src/api/connections.ts
@@ -7,7 +7,7 @@ export interface ClientConnection {
   groupOrTopic: string;
   protocol: string;
   address?: string | null;
-  language: string;
+  language?: string | null;
   version: string;
   connectedAt?: string | null;
   partial?: boolean;
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index b57616769..f907b0977 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -75,6 +75,7 @@ const translations: Record<string, Record<Lang, string>> = {
   'common.no': { zh: '否', en: 'No' },
   'common.retry': { zh: '重试', en: 'Retry' },
   'common.unavailable': { zh: '不可用', en: 'Unavailable' },
+  'common.unknown': { zh: '未知', en: 'Unknown' },
 
   // ─── Global layout controls ───
   'layout.skipToMain': { zh: '跳到主要内容', en: 'Skip to main content' },
diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
index 004de88c7..ecae50abd 100644
--- a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
@@ -390,6 +390,27 @@ describe('Clients page', () => {
     expect(within(dialog).getAllByText('-')).toHaveLength(2);
   });
 
+  it('labels a connection whose language the backend could not map', async () 
=> {
+    vi.mocked(connectionsService.listConnections).mockResolvedValue([
+      {
+        ...connection,
+        clientId: '[email protected]:49152',
+        groupOrTopic: 'ruby-topic',
+        language: null,
+      },
+    ]);
+    renderWithProviders(<ClientsPage />);
+
+    const rows = await screen.findAllByRole('row', { name: /ruby-topic/ });
+    const row = rows.find((candidate) => 
within(candidate).queryByRole('button', { name: /详情/ }));
+    expect(row).toBeDefined();
+    expect(within(row!).getByText('未知')).toBeInTheDocument();
+
+    const distribution = await 
screen.findByTestId('language-version-distribution');
+    expect(within(distribution).getByText('未知 5.0.7: 1')).toBeInTheDocument();
+    expect(within(distribution).queryByText(/null/)).not.toBeInTheDocument();
+  });
+
   it('exports the currently filtered client connections as CSV', async () => {
     const createObjectURL = vi.fn((blob: Blob | MediaSource) => {
       expect(blob).toBeInstanceOf(Blob);
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index 01c86ca37..6fc768aa3 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -165,6 +165,12 @@ function getLoadErrorMessage(error: unknown): string {
 
 const displayMetadata = (value: string | null | undefined) => value || '-';
 
+/**
+ * Bucket key for a connection whose broker-reported `LanguageCode` has no 
`ClientLanguage`
+ * counterpart, so the API sends `language: null`. The visible label is 
localized on render.
+ */
+const UNKNOWN_LANGUAGE = 'unknown';
+
 /* ═══════════════════════════════════════════
    ClientsPage
    ═══════════════════════════════════════════ */
@@ -314,7 +320,9 @@ const ClientsPage = () => {
       consumers: instances.filter((connection) => connection.type === 
'Consumer').length,
       protocols: countBy(instances.map((connection) => connection.protocol)),
       languageVersions: countBy(
-        instances.map((connection) => `${connection.language} 
${connection.version}`),
+        instances.map(
+          (connection) => `${connection.language ?? UNKNOWN_LANGUAGE} 
${connection.version}`,
+        ),
       ),
     };
   }, [clusterConnections]);
@@ -377,7 +385,7 @@ const ClientsPage = () => {
         matches('clusterName', connection.clusterName) &&
         matches('type', connection.type) &&
         matches('protocol', connection.protocol) &&
-        matches('language', connection.language),
+        matches('language', connection.language ?? ''),
     );
   }, [columnFilters, filtered]);
 
@@ -393,6 +401,15 @@ const ClientsPage = () => {
   /* ═══════════════════════════════════════════
      Table Columns (with built-in filters)
      ═══════════════════════════════════════════ */
+  const renderLanguageTag = (language?: string | null) => {
+    const config = languageConfig[language ?? ''];
+    return (
+      <Tag color={config?.color ?? 'default'}>
+        {config?.label ?? (language || t('common.unknown'))}
+      </Tag>
+    );
+  };
+
   const columns: ColumnsType<ClientConnection> = [
     {
       title: t('clients.cluster'),
@@ -490,10 +507,7 @@ const ClientsPage = () => {
       })),
       filteredValue: columnFilters.language ?? null,
       onFilter: (value, record) => record.language === value,
-      render: (lang: string) => {
-        const cfg = languageConfig[lang] ?? { color: 'default', label: lang };
-        return <Tag color={cfg.color}>{cfg.label}</Tag>;
-      },
+      render: (lang?: string | null) => renderLanguageTag(lang),
     },
     {
       title: t('common.version'),
@@ -839,7 +853,11 @@ const ClientsPage = () => {
               connectionStats.languageVersions.map(({ label, count }) => {
                 const [language, ...versionParts] = label.split(' ');
                 const version = versionParts.join(' ');
-                const config = languageConfig[language] ?? { color: 'default', 
label: language };
+                const config =
+                  languageConfig[language] ??
+                  (language === UNKNOWN_LANGUAGE
+                    ? { color: 'default', label: t('common.unknown') }
+                    : { color: 'default', label: language });
                 return (
                   <Tag key={label} color={config.color}>
                     {config.label} {version}: {count}
@@ -1007,9 +1025,7 @@ const ClientsPage = () => {
               </Text>
             </Descriptions.Item>
             <Descriptions.Item label={t('clients.language')}>
-              <Tag color={languageConfig[selectedConnection.language]?.color 
?? 'default'}>
-                {languageConfig[selectedConnection.language]?.label ?? 
selectedConnection.language}
-              </Tag>
+              {renderLanguageTag(selectedConnection.language)}
             </Descriptions.Item>
             <Descriptions.Item label={t('common.version')}>
               {selectedConnection.version}

Reply via email to