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 cc61356d2 fix(web): retain partial diagnostics and harden page 
rendering (#2221)
cc61356d2 is described below

commit cc61356d217365c8dfecd65badde832c53fd986e
Author: btlqql <[email protected]>
AuthorDate: Wed Aug 19 13:56:52 2026 +0800

    fix(web): retain partial diagnostics and harden page rendering (#2221)
    
    * fix(web): retain partial consumer group diagnostics
    
    * fix(clients): export only visible filtered rows
    
    * fix(web): harden nullable collection rendering
---
 .../pages/cluster/__tests__/ClientsPage.test.tsx   |  36 +++++
 web/src/pages/cluster/clients.tsx                  |  23 +++-
 web/src/pages/instance/consumer.tsx                |   6 +-
 web/src/pages/ops/alerts.tsx                       |   2 +-
 web/src/pages/studio/GroupManagement.tsx           | 146 +++++++++++++--------
 .../studio/__tests__/GroupManagement.test.tsx      |  44 +++++++
 6 files changed, 200 insertions(+), 57 deletions(-)

diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx 
b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
index 9e5f8b03b..b6b06a274 100644
--- a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
@@ -264,6 +264,42 @@ describe('Clients page', () => {
     expect(revokeObjectURL).toHaveBeenCalledWith('blob:client-connections');
   });
 
+  it('applies table column filters to the exported CSV', async () => {
+    const createObjectURL = vi.fn((blob: Blob | MediaSource) => {
+      expect(blob).toBeInstanceOf(Blob);
+      return 'blob:filtered-client-connections';
+    });
+    Object.defineProperty(URL, 'createObjectURL', {
+      writable: true,
+      value: createObjectURL,
+    });
+    Object.defineProperty(URL, 'revokeObjectURL', {
+      writable: true,
+      value: vi.fn(),
+    });
+    vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => 
{});
+    
vi.mocked(connectionsService.listConnections).mockResolvedValue(connections);
+    const user = userEvent.setup();
+    renderWithProviders(<ClientsPage />);
+
+    await screen.findByText('[email protected]:49152');
+    const filterTriggers = 
document.querySelectorAll<HTMLElement>('.ant-table-filter-trigger');
+    await user.click(filterTriggers[1]);
+    const filterDropdown = 
document.querySelector<HTMLElement>('.ant-table-filter-dropdown');
+    expect(filterDropdown).not.toBeNull();
+    await user.click(within(filterDropdown!).getByText('Consumer'));
+    await user.click(within(filterDropdown!).getByRole('button', { name: 'OK' 
}));
+
+    
expect(screen.queryByText('[email protected]:49152')).not.toBeInTheDocument();
+    await user.click(screen.getByRole('button', { name: '导出' }));
+
+    const blob = createObjectURL.mock.calls[0][0] as Blob;
+    const csv = await blob.text();
+    expect(csv).not.toContain('[email protected]:49152');
+    expect(csv).toContain('[email protected]:49153');
+    expect(csv).toContain('[email protected]:49154');
+  });
+
   it('renders empty distributions when no connections are available', async () 
=> {
     vi.mocked(connectionsService.listConnections).mockResolvedValue([]);
     renderWithProviders(<ClientsPage />);
diff --git a/web/src/pages/cluster/clients.tsx 
b/web/src/pages/cluster/clients.tsx
index 278bef5cb..4a8b9a86f 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -34,6 +34,7 @@ import {
 } from 'antd';
 import { DownloadSimple, Eye, MagnifyingGlass } from '@phosphor-icons/react';
 import type { ColumnsType } from 'antd/es/table';
+import type { TableProps } from 'antd';
 
 import PageHeader from '../../components/PageHeader';
 import { useLang } from '../../i18n/LangContext';
@@ -83,6 +84,8 @@ const CLIENT_CONNECTION_EXPORT_COLUMNS: 
CsvColumn<ClientConnection>[] = [
   { header: 'Partial', value: (connection) => (connection.partial ? 'true' : 
'false') },
 ];
 
+type ClientTableFilters = 
Parameters<NonNullable<TableProps<ClientConnection>['onChange']>>[1];
+
 const countBy = (values: string[]) =>
   [
     ...values.reduce(
@@ -130,6 +133,7 @@ const ClientsPage = () => {
   const [loadError, setLoadError] = useState<string | null>(null);
   const [registryLoadKey, setRegistryLoadKey] = useState(0);
   const [connectionLoadKey, setConnectionLoadKey] = useState(0);
+  const [columnFilters, setColumnFilters] = useState<ClientTableFilters>({});
 
   const selectedCluster = registryClusters.find((cluster) => cluster.endpoint 
=== selectedEndpoint);
 
@@ -263,9 +267,23 @@ const ClientsPage = () => {
     );
   }, [clusterConnections, search]);
 
+  const exportConnections = useMemo(() => {
+    const matches = (key: string, value: string) => {
+      const selected = columnFilters[key];
+      return !selected?.length || selected.some((filterValue) => 
String(filterValue) === value);
+    };
+    return filtered.filter(
+      (connection) =>
+        matches('clusterName', connection.clusterName) &&
+        matches('type', connection.type) &&
+        matches('protocol', connection.protocol) &&
+        matches('language', connection.language),
+    );
+  }, [columnFilters, filtered]);
+
   const handleExport = () => {
     const filename = `rocketmq-client-connections-${new 
Date().toISOString().slice(0, 10)}.csv`;
-    const csv = buildCsv(CLIENT_CONNECTION_EXPORT_COLUMNS, filtered);
+    const csv = buildCsv(CLIENT_CONNECTION_EXPORT_COLUMNS, exportConnections);
     downloadCsv(filename, csv);
   };
 
@@ -476,7 +494,7 @@ const ClientsPage = () => {
         </Space>
         <Button
           icon={<DownloadSimple size={16} />}
-          disabled={filtered.length === 0}
+          disabled={exportConnections.length === 0}
           onClick={handleExport}
         >
           {t('common.export')}
@@ -553,6 +571,7 @@ const ClientsPage = () => {
             
`${connection.type}:${connection.clientId}:${connection.groupOrTopic}`
           }
           loading={loading}
+          onChange={(_, filters) => setColumnFilters(filters)}
           scroll={{ x: 1320 }}
           pagination={{
             pageSize: 20,
diff --git a/web/src/pages/instance/consumer.tsx 
b/web/src/pages/instance/consumer.tsx
index 62e57c173..ef7c038e6 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -949,7 +949,7 @@ const ConsumerPageContent = ({
                   dataSource={
                     
subscriptionsByGroup[diagnosticCacheKey(selectedInstanceId, record.name)] ?? []
                   }
-                  rowKey="topic"
+                  rowKey={(record) => 
`${record.topic}-${record.filterMode}-${record.expression}`}
                   loading={
                     
subscriptionLoadingByGroup[diagnosticCacheKey(selectedInstanceId, record.name)]
                   }
@@ -1177,7 +1177,9 @@ const ConsumerPageContent = ({
                       <Table
                         columns={subscriptionSubColumns}
                         dataSource={visibleSubscriptions}
-                        rowKey="topic"
+                        rowKey={(record) =>
+                          
`${record.topic}-${record.filterMode}-${record.expression}`
+                        }
                         
loading={subscriptionLoadingByGroup[selectedDiagnosticKey]}
                         pagination={false}
                         size="small"
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index b4b5cbb60..d29fb091d 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -260,7 +260,7 @@ const AlertsPage = () => {
       title: t('alerts.channels'),
       render: (_, record) => (
         <Flex gap={4} wrap="wrap">
-          {record.channels.map((ch) => (
+          {(record.channels ?? []).map((ch) => (
             <Tag key={ch} color={channelColors[ch]}>
               {channelLabels[ch]}
             </Tag>
diff --git a/web/src/pages/studio/GroupManagement.tsx 
b/web/src/pages/studio/GroupManagement.tsx
index c297b21ec..29038d6c4 100644
--- a/web/src/pages/studio/GroupManagement.tsx
+++ b/web/src/pages/studio/GroupManagement.tsx
@@ -30,6 +30,7 @@ import {
   Space,
   Switch,
   message,
+  Alert,
 } from 'antd';
 import { MagnifyingGlass, ArrowClockwise, Users } from '@phosphor-icons/react';
 import { useLang } from '../../i18n/LangContext';
@@ -66,7 +67,10 @@ const GroupManagementPage = () => {
   const [loading, setLoading] = useState(true);
   const [subscriptions, setSubscriptions] = useState<SubscriptionEntry[]>([]);
   const [progress, setProgress] = useState<QueueProgress[]>([]);
-  const [detailLoading, setDetailLoading] = useState(false);
+  const [subscriptionLoading, setSubscriptionLoading] = useState(false);
+  const [progressLoading, setProgressLoading] = useState(false);
+  const [subscriptionError, setSubscriptionError] = useState<string | 
null>(null);
+  const [progressError, setProgressError] = useState<string | null>(null);
   const listRequestId = useRef(0);
   const listInFlight = useRef<Promise<void> | null>(null);
   const listRefreshQueued = useRef(false);
@@ -135,23 +139,41 @@ const GroupManagementPage = () => {
       setModalVisible(true);
       setSubscriptions([]);
       setProgress([]);
-      setDetailLoading(true);
-      try {
-        const [subs, prog] = await Promise.all([
-          getConsumerSubscriptions(group.name, group.instanceId),
-          getConsumerProgress(group.name, group.instanceId),
-        ]);
-        if (requestId !== detailRequestId.current) return;
-        setSubscriptions(subs);
-        setProgress(prog);
-      } catch {
-        if (requestId !== detailRequestId.current) return;
-        message.error(t('consumer.fetchProgressFailed', { name: group.name }));
-      } finally {
-        if (requestId === detailRequestId.current) {
-          setDetailLoading(false);
-        }
-      }
+      setSubscriptionError(null);
+      setProgressError(null);
+      setSubscriptionLoading(true);
+      setProgressLoading(true);
+
+      const subscriptionRequest = getConsumerSubscriptions(group.name, 
group.instanceId)
+        .then(
+          (result) => {
+            if (requestId === detailRequestId.current) 
setSubscriptions(result);
+          },
+          () => {
+            if (requestId === detailRequestId.current) {
+              setSubscriptionError(t('consumer.fetchSubscriptionsFailed', { 
name: group.name }));
+            }
+          },
+        )
+        .finally(() => {
+          if (requestId === detailRequestId.current) 
setSubscriptionLoading(false);
+        });
+      const progressRequest = getConsumerProgress(group.name, group.instanceId)
+        .then(
+          (result) => {
+            if (requestId === detailRequestId.current) setProgress(result);
+          },
+          () => {
+            if (requestId === detailRequestId.current) {
+              setProgressError(t('consumer.fetchProgressFailed', { name: 
group.name }));
+            }
+          },
+        )
+        .finally(() => {
+          if (requestId === detailRequestId.current) setProgressLoading(false);
+        });
+
+      await Promise.all([subscriptionRequest, progressRequest]);
     },
     [t],
   );
@@ -434,11 +456,19 @@ const GroupManagementPage = () => {
                     <h4 style={{ marginTop: 20, marginBottom: 12 }}>
                       {t('groupMgmt.subscription')}
                     </h4>
+                    {subscriptionError && (
+                      <Alert
+                        type="error"
+                        showIcon
+                        message={subscriptionError}
+                        style={{ marginBottom: 12 }}
+                      />
+                    )}
                     <Table
                       columns={subscriptionColumns}
                       dataSource={subscriptions}
                       rowKey="topic"
-                      loading={detailLoading}
+                      loading={subscriptionLoading}
                       pagination={false}
                       size="small"
                     />
@@ -475,39 +505,51 @@ const GroupManagementPage = () => {
                 key: 'progress',
                 label: t('groupMgmt.consumeProgress'),
                 children: (
-                  <Table
-                    columns={[
-                      { title: 'Broker', dataIndex: 'broker', key: 'broker' },
-                      { title: 'QueueId', dataIndex: 'queueId', key: 'queueId' 
},
-                      {
-                        title: 'Broker Offset',
-                        dataIndex: 'brokerOffset',
-                        key: 'brokerOffset',
-                        render: (v: number) => v.toLocaleString(),
-                      },
-                      {
-                        title: 'Consumer Offset',
-                        dataIndex: 'consumerOffset',
-                        key: 'consumerOffset',
-                        render: (v: number) => v.toLocaleString(),
-                      },
-                      {
-                        title: 'Diff',
-                        dataIndex: 'diffTotal',
-                        key: 'diffTotal',
-                        render: (v: number) => (
-                          <span style={{ color: v > 100 ? '#ff4d4f' : 
'#52c41a', fontWeight: 500 }}>
-                            {v.toLocaleString()}
-                          </span>
-                        ),
-                      },
-                    ]}
-                    dataSource={progress}
-                    rowKey={(record) => `${record.broker}-${record.queueId}`}
-                    loading={detailLoading}
-                    pagination={false}
-                    size="small"
-                  />
+                  <>
+                    {progressError && (
+                      <Alert
+                        type="error"
+                        showIcon
+                        message={progressError}
+                        style={{ marginBottom: 12 }}
+                      />
+                    )}
+                    <Table
+                      columns={[
+                        { title: 'Broker', dataIndex: 'broker', key: 'broker' 
},
+                        { title: 'QueueId', dataIndex: 'queueId', key: 
'queueId' },
+                        {
+                          title: 'Broker Offset',
+                          dataIndex: 'brokerOffset',
+                          key: 'brokerOffset',
+                          render: (v: number) => v.toLocaleString(),
+                        },
+                        {
+                          title: 'Consumer Offset',
+                          dataIndex: 'consumerOffset',
+                          key: 'consumerOffset',
+                          render: (v: number) => v.toLocaleString(),
+                        },
+                        {
+                          title: 'Diff',
+                          dataIndex: 'diffTotal',
+                          key: 'diffTotal',
+                          render: (v: number) => (
+                            <span
+                              style={{ color: v > 100 ? '#ff4d4f' : '#52c41a', 
fontWeight: 500 }}
+                            >
+                              {v.toLocaleString()}
+                            </span>
+                          ),
+                        },
+                      ]}
+                      dataSource={progress}
+                      rowKey={(record) => `${record.broker}-${record.queueId}`}
+                      loading={progressLoading}
+                      pagination={false}
+                      size="small"
+                    />
+                  </>
                 ),
               },
             ]}
diff --git a/web/src/pages/studio/__tests__/GroupManagement.test.tsx 
b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
index 2701533b5..2e2d1d9c3 100644
--- a/web/src/pages/studio/__tests__/GroupManagement.test.tsx
+++ b/web/src/pages/studio/__tests__/GroupManagement.test.tsx
@@ -199,6 +199,50 @@ describe('GroupManagement Page', () => {
     expect(screen.queryByText('FIRST_GROUP_TOPIC')).not.toBeInTheDocument();
   });
 
+  it('keeps subscriptions when progress loading fails', async () => {
+    vi.mocked(consumerService.getConsumerSubscriptions).mockResolvedValue([
+      {
+        topic: 'AVAILABLE_SUBSCRIPTION',
+        expression: '*',
+        type: 'TAG',
+        filterMode: 'TAG',
+        consistency: 'consistent',
+      },
+    ]);
+    vi.mocked(consumerService.getConsumerProgress).mockRejectedValue(new 
Error('unavailable'));
+
+    const user = userEvent.setup();
+    renderWithProviders(<GroupManagement />);
+    await user.click(await screen.findByText('order-consumer-group'));
+
+    expect(await 
screen.findByText('AVAILABLE_SUBSCRIPTION')).toBeInTheDocument();
+    const dialog = await screen.findByRole('dialog');
+    await user.click(within(dialog).getAllByRole('tab')[2]);
+    expect(document.querySelector('.ant-alert-error')).toBeInTheDocument();
+  });
+
+  it('keeps progress when subscription loading fails', async () => {
+    vi.mocked(consumerService.getConsumerSubscriptions).mockRejectedValue(new 
Error('unavailable'));
+    vi.mocked(consumerService.getConsumerProgress).mockResolvedValue([
+      {
+        broker: 'broker-a',
+        queueId: 0,
+        brokerOffset: 20,
+        consumerOffset: 10,
+        diffTotal: 10,
+      },
+    ]);
+
+    const user = userEvent.setup();
+    renderWithProviders(<GroupManagement />);
+    await user.click(await screen.findByText('order-consumer-group'));
+    const dialog = await screen.findByRole('dialog');
+    await user.click(within(dialog).getAllByRole('tab')[2]);
+
+    expect(await screen.findByText('broker-a')).toBeInTheDocument();
+    expect(document.querySelector('.ant-alert-error')).toBeInTheDocument();
+  });
+
   it('queues one refresh instead of overlapping an active group request', 
async () => {
     const initialGroups = createDeferred<ConsumerGroup[]>();
     const refreshedGroups = createDeferred<ConsumerGroup[]>();

Reply via email to