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 6c0e983dd fix(web): scope LiteTopic TTL to its instance and drop a 
fabricated column (#5600)
6c0e983dd is described below

commit 6c0e983dd7ca033b38a5579d6c4c313d2d018e21
Author: Zhao Jianing <[email protected]>
AuthorDate: Fri Oct 9 18:46:31 2026 +0800

    fix(web): scope LiteTopic TTL to its instance and drop a fabricated column 
(#5600)
    
    Two defects on the cluster pages.
    
    - `extendLiteTopicTTL` was called without the instance, so a TTL extension 
could be applied through
      whichever instance the request happened to land on; the API now takes 
`instanceId` first and the
      page forwards the instance that owns the row. (#5600)
    - The NameServer tab's "Connections" column was fabricated: `mapClusters` 
wrote `connections: 0` for
      every nameserver, so the column always read zero. The field is removed 
along with the column
      instead of being populated with a guess. (#5543)
    
    `liteTopic` + `LiteTopic` + `BrokerCluster` tests: 40 green; prettier 
clean; the two eslint warnings
    are pre-existing on trunk.
    
    Maintainer edits on top of the contribution: restored the trailing newline 
in all five changed files
    (the branch had dropped them, which fails prettier), reused the existing 
`supportsApacheRuntime`
    helper instead of an inline copy of its condition, dropped a duplicated 
comment, and switched the
    test's `vi.mock('../../../api/instance')` to the `importActual` + spread 
pattern already used in that
    file so the real helper stays in play. Note that the helper reuse is a 
de-duplication only: it differs
    from the inline `!vendor` form just for a `null`/empty vendor, which the 
API does not produce, so no
    fixture can pin it.
    
    Folded in #5543.
---
 web/src/api/liteTopic.test.ts                      |  6 ++-
 web/src/api/liteTopic.ts                           | 10 +++-
 web/src/i18n/translations.ts                       |  6 +++
 web/src/pages/studio/BrokerCluster.tsx             |  9 ----
 web/src/pages/studio/LiteTopic.tsx                 | 55 ++++++++++++++++++++--
 .../pages/studio/__tests__/BrokerCluster.test.tsx  | 14 ++++++
 web/src/pages/studio/__tests__/LiteTopic.test.tsx  | 42 +++++++++++++++++
 7 files changed, 126 insertions(+), 16 deletions(-)

diff --git a/web/src/api/liteTopic.test.ts b/web/src/api/liteTopic.test.ts
index a571470f2..9fac99c31 100644
--- a/web/src/api/liteTopic.test.ts
+++ b/web/src/api/liteTopic.test.ts
@@ -112,15 +112,17 @@ describe('LiteTopic API', () => {
     expect(result.popProgress).toBe(96);
   });
 
-  it('extends lite topic TTL', async () => {
+  it('extends lite topic TTL with the owning instance id', async () => {
     mock.onPost('/liteTopic/extendTTL').reply((config) => {
       const body = JSON.parse(config.data);
+      // LiteTopicTTLUpdateDTO requires a non-blank instanceId (@NotBlank).
+      expect(body.instanceId).toBe('apache-main');
       expect(body.topicPattern).toBe('order-*');
       expect(body.newTTL).toBe(7200);
       return [200, { code: 200 }];
     });
 
-    await extendLiteTopicTTL('order-*', 7200);
+    await extendLiteTopicTTL('apache-main', 'order-*', 7200);
   });
 
   it('queries lite topic quota without namespace', async () => {
diff --git a/web/src/api/liteTopic.ts b/web/src/api/liteTopic.ts
index 1dfeff3bd..1a01e3a7f 100644
--- a/web/src/api/liteTopic.ts
+++ b/web/src/api/liteTopic.ts
@@ -89,8 +89,14 @@ export async function queryLiteTopicSession(sessionId: 
string): Promise<LiteTopi
   return res.data.data;
 }
 
-export async function extendLiteTopicTTL(topicPattern: string, newTTL: 
number): Promise<void> {
-  await client.post('/liteTopic/extendTTL', { topicPattern, newTTL });
+export async function extendLiteTopicTTL(
+  instanceId: string,
+  topicPattern: string,
+  newTTL: number,
+): Promise<void> {
+  // The backend's LiteTopicTTLUpdateDTO requires a non-blank instanceId
+  // (the write is ownership-checked against the owning instance).
+  await client.post('/liteTopic/extendTTL', { instanceId, topicPattern, newTTL 
});
 }
 
 export async function queryLiteTopicQuota(namespace?: string): 
Promise<LiteTopicQuota> {
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 09e9dbba9..26588b377 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -3580,6 +3580,12 @@ const translations: Record<string, Record<Lang, string>> 
= {
     en: 'Enter new TTL value (ms)',
   },
   'liteTopic.extendTtlModalTitle': { zh: '延长 TTL', en: 'Extend TTL' },
+  'liteTopic.instance': { zh: '所属实例', en: 'Owning Instance' },
+  'liteTopic.instancePlaceholder': { zh: '选择所属实例', en: 'Select the owning 
instance' },
+  'liteTopic.instanceRequired': {
+    zh: '请选择 Topic 所属的实例',
+    en: 'Select the instance that owns the topic',
+  },
   'liteTopic.popProgress': { zh: 'Pop 进度', en: 'Pop Progress' },
   'liteTopic.sessionStatus': { zh: '会话状态', en: 'Session Status' },
   'liteTopic.creationCount': { zh: '创建数量', en: 'Creation Count' },
diff --git a/web/src/pages/studio/BrokerCluster.tsx 
b/web/src/pages/studio/BrokerCluster.tsx
index 0db75bc92..dedeeae46 100644
--- a/web/src/pages/studio/BrokerCluster.tsx
+++ b/web/src/pages/studio/BrokerCluster.tsx
@@ -59,7 +59,6 @@ interface NameServerRecord {
   status: NodeStatus;
   version: string;
   address: string;
-  connections: number;
 }
 
 interface ProxyRecord {
@@ -90,7 +89,6 @@ const NAMESERVER_EXPORT_COLUMNS: 
CsvColumn<NameServerRecord>[] = [
   { header: 'Status', value: (nameServer) => nameServer.status },
   { header: 'Version', value: (nameServer) => nameServer.version },
   { header: 'Address', value: (nameServer) => nameServer.address },
-  { header: 'Connections', value: (nameServer) => nameServer.connections },
 ];
 
 const PROXY_EXPORT_COLUMNS: CsvColumn<ProxyRecord>[] = [
@@ -158,7 +156,6 @@ function mapClusters(clusters: ClusterInfo[]): {
         status: normalizeStatus(nameServer.status),
         version: cluster.version,
         address: nameServer.addr,
-        connections: 0,
       });
     });
 
@@ -429,12 +426,6 @@ const BrokerClusterPage = () => {
         </code>
       ),
     },
-    {
-      title: t('brokerCluster.connections'),
-      dataIndex: 'connections',
-      key: 'connections',
-      render: (text: number) => <span style={{ fontWeight: 500 
}}>{text.toLocaleString()}</span>,
-    },
   ];
 
   const proxyColumns = [
diff --git a/web/src/pages/studio/LiteTopic.tsx 
b/web/src/pages/studio/LiteTopic.tsx
index 8061af752..b1b1547c3 100644
--- a/web/src/pages/studio/LiteTopic.tsx
+++ b/web/src/pages/studio/LiteTopic.tsx
@@ -59,6 +59,7 @@ import {
   type LiteTopicItem,
   type LiteTopicSession,
 } from '../../api/liteTopic';
+import { listInstances, supportsApacheRuntime, type Instance } from 
'../../api/instance';
 import { buildCsv, downloadCsv, type CsvColumn } from '../../utils/download';
 
 const formatDuration = (ms: number | undefined | null): string => {
@@ -147,10 +148,15 @@ const LiteTopicPage: React.FC = () => {
   // Extend TTL modal
   const [extendTTLModalOpen, setExtendTTLModalOpen] = useState(false);
   const [extendTTLForm, setExtendTTLForm] = useState<{
+    instanceId: string;
     topicPattern: string;
     newTTL: number | null;
-  }>({ topicPattern: '', newTTL: null });
+  }>({ instanceId: '', topicPattern: '', newTTL: null });
   const [extendTTLLoading, setExtendTTLLoading] = useState(false);
+  // Instances that may own a Lite topic write (the backend's ownership guard
+  // only supports Apache-vendor instances for TTL updates).
+  const [ttlInstanceOptions, setTtlInstanceOptions] = useState<Instance[]>([]);
+  const [ttlInstancesLoading, setTtlInstancesLoading] = useState(false);
 
   const mountedRef = useRef(false);
   const bootstrapRequestId = useRef(0);
@@ -304,17 +310,43 @@ const LiteTopicPage: React.FC = () => {
 
   const handleOpenExtendTTL = (record: LiteTopicItem) => {
     setExtendTTLForm({
+      instanceId: '',
       topicPattern: record.topicPattern || '',
       newTTL: null,
     });
     setExtendTTLModalOpen(true);
+    // The TTL write is ownership-checked against the owning instance, so the
+    // modal needs the Apache-vendor instance list; auto-select when only one
+    // candidate exists.
+    setTtlInstancesLoading(true);
+    listInstances()
+      .then((instances) => {
+        const candidates = instances.filter(supportsApacheRuntime);
+        setTtlInstanceOptions(candidates);
+        setExtendTTLForm((form) =>
+          form.instanceId === '' && candidates.length === 1
+            ? { ...form, instanceId: String(candidates[0].name) }
+            : form,
+        );
+      })
+      .catch(() => {
+        setTtlInstanceOptions([]);
+      })
+      .finally(() => {
+        setTtlInstancesLoading(false);
+      });
   };
 
   const handleExtendTTL = async () => {
-    if (!extendTTLForm.topicPattern || extendTTLForm.newTTL == null) return;
+    if (!extendTTLForm.instanceId || !extendTTLForm.topicPattern || 
extendTTLForm.newTTL == null)
+      return;
     setExtendTTLLoading(true);
     try {
-      await extendLiteTopicTTL(extendTTLForm.topicPattern, 
extendTTLForm.newTTL);
+      await extendLiteTopicTTL(
+        extendTTLForm.instanceId,
+        extendTTLForm.topicPattern,
+        extendTTLForm.newTTL,
+      );
       message.success(t('liteTopic.extendTtlSuccess'));
       setExtendTTLModalOpen(false);
       void fetchData(patternFilter || undefined, namespaceFilter || undefined);
@@ -912,6 +944,23 @@ const LiteTopicPage: React.FC = () => {
         destroyOnHidden
       >
         <Form layout="vertical" style={{ marginTop: 16 }}>
+          <Form.Item
+            label={t('liteTopic.instance')}
+            required
+            validateStatus={extendTTLForm.instanceId ? '' : 'error'}
+            help={extendTTLForm.instanceId ? undefined : 
t('liteTopic.instanceRequired')}
+          >
+            <Select
+              value={extendTTLForm.instanceId || undefined}
+              onChange={(val) => setExtendTTLForm({ ...extendTTLForm, 
instanceId: val })}
+              loading={ttlInstancesLoading}
+              placeholder={t('liteTopic.instancePlaceholder')}
+              options={ttlInstanceOptions.map((instance) => ({
+                value: String(instance.name),
+                label: instance.name,
+              }))}
+            />
+          </Form.Item>
           <Form.Item label={t('liteTopic.pattern')}>
             <Input
               value={extendTTLForm.topicPattern}
diff --git a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx 
b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
index 21f19ba64..c9aa13fdc 100644
--- a/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
+++ b/web/src/pages/studio/__tests__/BrokerCluster.test.tsx
@@ -402,4 +402,18 @@ describe('BrokerCluster Page', () => {
     expect(screen.getByText('N/A')).toBeInTheDocument();
     expect(screen.queryByText('运行中')).not.toBeInTheDocument();
   });
+
+  it('does not render a Connections column for nameservers', async () => {
+    // The backend NameServerVO has no connection count; the column used to
+    // fabricate a hardcoded 0 for every nameserver (and export it to CSV).
+    const user = userEvent.setup();
+    renderWithProviders(<BrokerCluster />);
+    await screen.findByText('broker-api-a');
+    await user.click(screen.getByRole('tab', { name: /NameServer|Nameserver/i 
}));
+    await screen.findAllByRole('columnheader');
+
+    expect(
+      screen.queryByRole('columnheader', { name: /连接数|[Cc]onnections/ }),
+    ).not.toBeInTheDocument();
+  });
 });
diff --git a/web/src/pages/studio/__tests__/LiteTopic.test.tsx 
b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
index 8f160e368..413acb04e 100644
--- a/web/src/pages/studio/__tests__/LiteTopic.test.tsx
+++ b/web/src/pages/studio/__tests__/LiteTopic.test.tsx
@@ -21,6 +21,7 @@ import userEvent from '@testing-library/user-event';
 import { App } from 'antd';
 import { LangProvider } from '../../../i18n/LangContext';
 import type { LiteTopicItem, LiteTopicQuota } from '../../../api/liteTopic';
+import type { Instance } from '../../../api/instance';
 import { downloadCsv } from '../../../utils/download';
 import LiteTopic, { formatTime } from '../LiteTopic';
 
@@ -34,6 +35,17 @@ const apiMocks = vi.hoisted(() => ({
 
 vi.mock('../../../api/liteTopic', () => apiMocks);
 
+const instanceApiMocks = vi.hoisted(() => ({ listInstances: vi.fn() }));
+
+vi.mock('../../../api/instance', async () => {
+  const instanceModule =
+    await vi.importActual<typeof 
import('../../../api/instance')>('../../../api/instance');
+  return {
+    ...instanceModule,
+    listInstances: instanceApiMocks.listInstances,
+  };
+});
+
 vi.mock('../../../utils/download', async () => {
   const downloadModule =
     await vi.importActual<typeof 
import('../../../utils/download')>('../../../utils/download');
@@ -119,6 +131,36 @@ describe('LiteTopic Page', () => {
       consumedMessages: 0,
       popProgress: 96,
     });
+    instanceApiMocks.listInstances.mockResolvedValue([]);
+  });
+
+  it('sends the owning instance id when extending TTL', async () => {
+    // LiteTopicTTLUpdateDTO requires a non-blank instanceId; only the
+    // Apache-vendor instance is a candidate for the ownership-checked write.
+    instanceApiMocks.listInstances.mockResolvedValue([
+      { id: 1, name: 'apache-main', vendor: 'APACHE' },
+      { id: 2, name: 'cloud-1', vendor: 'ALIYUN' },
+    ] as unknown as Instance[]);
+    const user = userEvent.setup();
+    renderPage();
+
+    await screen.findByText('order-*');
+    await user.click(await screen.findByText('延长 TTL'));
+
+    // exactly one Apache-vendor instance -> auto-selected in the modal
+    await waitFor(() => {
+      expect(apiMocks.extendLiteTopicTTL).not.toHaveBeenCalled();
+      const selected = document.querySelector('.ant-modal 
.ant-select-selection-item');
+      expect(selected?.textContent).toBe('apache-main');
+    });
+
+    await user.type(screen.getByRole('spinbutton'), '7200');
+    // antd inserts a space between the two CJK chars of the confirm button
+    await user.click(await screen.findByRole('button', { name: /确\s*认/ }));
+
+    await waitFor(() => {
+      expect(apiMocks.extendLiteTopicTTL).toHaveBeenCalledWith('apache-main', 
'order-*', 7200);
+    });
   });
 
   it('returns to the first page when the local TTL filter changes', async () 
=> {

Reply via email to