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 895498a7b fix(client): use backend connection filters (#3294)
895498a7b is described below
commit 895498a7bce6b3cda4975a1c3ebbbd48c8c9ceee
Author: xdz997 <[email protected]>
AuthorDate: Tue Sep 8 15:23:42 2026 +0800
fix(client): use backend connection filters (#3294)
---
.../pages/cluster/__tests__/ClientsPage.test.tsx | 66 ++++++++++++++++++++--
web/src/pages/cluster/clients.tsx | 38 +++++++++----
2 files changed, 87 insertions(+), 17 deletions(-)
diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
index ad7cb4042..e338a4f14 100644
--- a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
+++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx
@@ -16,7 +16,7 @@
*/
import { App } from 'antd';
-import { act, render, screen, waitFor, within } from '@testing-library/react';
+import { act, fireEvent, render, screen, waitFor, within } from
'@testing-library/react';
import userEvent from '@testing-library/user-event';
import type React from 'react';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from
'vitest';
@@ -139,6 +139,17 @@ const renderWithProviders = (ui: React.ReactElement) =>
</App>,
);
+const selectOption = async (user: ReturnType<typeof userEvent.setup>, label:
string) => {
+ const option = await waitFor(() => {
+ const match = [
+
...document.querySelectorAll<HTMLElement>('.ant-select-item-option-content'),
+ ].find((element) => element.textContent === label);
+ if (!match) throw new Error(`Select option not found: ${label}`);
+ return match;
+ });
+ await user.click(option);
+};
+
const deferred = <T,>() => {
let resolve!: (value: T) => void;
let reject!: (reason?: unknown) => void;
@@ -178,6 +189,34 @@ describe('Clients page', () => {
});
});
+ it('sends cluster and type filters to the backend', async () => {
+ const mixedConnections = [
+ { ...connection, clusterName: 'ns-prod', type: 'Producer' },
+ { ...connection, clusterName: 'ns-prod', type: 'Consumer' },
+ ];
+
vi.mocked(connectionsService.listConnections).mockResolvedValue(mixedConnections);
+ const user = userEvent.setup();
+ renderWithProviders(<ClientsPage />);
+
+ await screen.findAllByText('[email protected]:49152');
+
+ const clusterSelect = screen.getAllByLabelText('所属集群')[0];
+ fireEvent.mouseDown(clusterSelect.querySelector('.ant-select-selector')!);
+ await selectOption(user, 'ns-prod');
+
+ const typeSelect = screen.getAllByLabelText('类型')[0];
+ fireEvent.mouseDown(typeSelect.querySelector('.ant-select-selector')!);
+ await selectOption(user, 'Consumer');
+
+ await waitFor(() => {
+ expect(connectionsService.listConnections).toHaveBeenLastCalledWith({
+ namesrvAddr: 'namesrv-1:9876',
+ clusterId: 'ns-prod',
+ type: 'Consumer',
+ });
+ });
+ });
+
it('summarizes connection types, protocols, and language versions', async ()
=> {
vi.mocked(connectionsService.listConnections).mockResolvedValue(connections);
renderWithProviders(<ClientsPage />);
@@ -247,14 +286,29 @@ describe('Clients page', () => {
it('updates statistics when the selected cluster filter changes', async ()
=> {
const user = userEvent.setup();
-
vi.mocked(connectionsService.listConnections).mockResolvedValue(connections);
+ vi.mocked(connectionsService.listConnections).mockImplementation((query) =>
+ Promise.resolve(
+ query?.clusterId === 'ns-prod'
+ ? connections.filter((item) => item.clusterName === 'ns-prod')
+ : connections,
+ ),
+ );
renderWithProviders(<ClientsPage />);
- await screen.findByText('[email protected]:49154');
- await user.click(screen.getByRole('combobox', { name: '所属集群' }));
- await user.click(
- await screen.findByText('ns-prod', { selector:
'.ant-select-item-option-content' }),
+ await screen.findAllByText('[email protected]:49154');
+ const clusterSelect = screen.getAllByLabelText('所属集群')[0];
+ fireEvent.mouseDown(clusterSelect.querySelector('.ant-select-selector')!);
+ await selectOption(user, 'ns-prod');
+
+ await waitFor(() =>
+ expect(connectionsService.listConnections).toHaveBeenLastCalledWith({
+ namesrvAddr: 'namesrv-1:9876',
+ clusterId: 'ns-prod',
+ }),
);
+ await act(async () => {
+ await Promise.resolve();
+ });
await waitFor(() => {
expect(within(screen.getByTestId('connection-total')).getByText('2')).toBeInTheDocument();
diff --git a/web/src/pages/cluster/clients.tsx
b/web/src/pages/cluster/clients.tsx
index 74efbd225..6dcd69ae9 100644
--- a/web/src/pages/cluster/clients.tsx
+++ b/web/src/pages/cluster/clients.tsx
@@ -163,6 +163,7 @@ const ClientsPage = () => {
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState('');
const [clusterFilter, setClusterFilter] = useState<string>('ALL');
+ const [typeFilter, setTypeFilter] = useState<string>('ALL');
const [selectedConnection, setSelectedConnection] =
useState<ClientConnection | null>(null);
const [loadError, setLoadError] = useState<string | null>(null);
const [registryLoadKey, setRegistryLoadKey] = useState(0);
@@ -231,7 +232,11 @@ const ClientsPage = () => {
if (connectionRequestRef.current === requestId) setLoading(true);
});
- void listConnections({ namesrvAddr: selectedEndpoint })
+ void listConnections({
+ namesrvAddr: selectedEndpoint,
+ clusterId: clusterFilter === 'ALL' ? undefined : clusterFilter,
+ type: typeFilter === 'ALL' ? undefined : typeFilter,
+ })
.then((nextConnections) => {
if (connectionRequestRef.current === requestId) {
setConnections(nextConnections);
@@ -249,7 +254,7 @@ const ClientsPage = () => {
.finally(() => {
if (connectionRequestRef.current === requestId) setLoading(false);
});
- }, [connectionLoadKey, selectedEndpoint, selectedCluster]);
+ }, [connectionLoadKey, clusterFilter, selectedEndpoint, selectedCluster,
typeFilter]);
useEffect(
() => () => {
@@ -262,21 +267,18 @@ const ClientsPage = () => {
/* ─── Cluster options using nsClusterName ─── */
const clusterOptions = useMemo(() => {
const clusterNames = [
- ...new Set(connections.map((connection) => connection.clusterName)),
+ ...new Set([
+ ...registryClusters.map((cluster) =>
cluster.nsClusterName).filter(Boolean),
+ ...connections.map((connection) =>
connection.clusterName).filter(Boolean),
+ ]),
].sort();
return [
{ value: 'ALL', label: t('clients.allClusters') },
...clusterNames.map((name) => ({ value: name, label: name })),
];
- }, [connections, t]);
+ }, [connections, registryClusters, t]);
- const clusterConnections = useMemo(
- () =>
- clusterFilter === 'ALL'
- ? connections
- : connections.filter((connection) => connection.clusterName ===
clusterFilter),
- [connections, clusterFilter],
- );
+ const clusterConnections = useMemo(() => connections, [connections]);
const connectionStats = useMemo(() => {
const instances = Array.from(
@@ -707,6 +709,20 @@ const ClientsPage = () => {
style={{ width: 180 }}
options={clusterOptions}
/>
+ <Select
+ aria-label={t('common.type')}
+ value={typeFilter}
+ onChange={(value) => {
+ setTypeFilter(value);
+ setCurrentPage(1);
+ }}
+ style={{ width: 140 }}
+ options={[
+ { value: 'ALL', label: t('common.all') },
+ { value: 'Producer', label: typeConfig.Producer?.label ??
'Producer' },
+ { value: 'Consumer', label: typeConfig.Consumer?.label ??
'Consumer' },
+ ]}
+ />
<Input.Search
placeholder={t('clients.searchPlaceholder')}
allowClear