From 692cee56511e799baf360c9f9bdd86affb17c4f1 Mon Sep 17 00:00:00 2001 From: liuhy Date: Sat, 25 Jul 2026 08:14:14 -0700 Subject: [PATCH] Add client connection detail modal --- web/src/i18n/translations.ts | 1 + .../cluster/__tests__/ClientsPage.test.tsx | 96 +++++++++++++++++++ web/src/pages/cluster/clients.tsx | 81 +++++++++++++++- 3 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 web/src/pages/cluster/__tests__/ClientsPage.test.tsx diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts index 3b37ff10..96d85c62 100644 --- a/web/src/i18n/translations.ts +++ b/web/src/i18n/translations.ts @@ -187,6 +187,7 @@ const translations: Record> = { 'clients.cluster': { zh: '所属集群', en: 'Cluster' }, 'clients.allClusters': { zh: '全部集群', en: 'All Clusters' }, 'clients.searchPlaceholder': { zh: '搜索 Client ID 或地址', en: 'Search Client ID or address' }, + 'clients.detailTitle': { zh: '客户端详情 - {id}', en: 'Client Detail - {id}' }, // ─── Alert Rules ─── 'alerts.title': { zh: '告警规则管理', en: 'Alert Rules' }, diff --git a/web/src/pages/cluster/__tests__/ClientsPage.test.tsx b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx new file mode 100644 index 00000000..686dc804 --- /dev/null +++ b/web/src/pages/cluster/__tests__/ClientsPage.test.tsx @@ -0,0 +1,96 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { App } from 'antd'; +import { render, screen, 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'; +import type { ClientConnection } from '../../../api/connections'; +import { LangProvider } from '../../../i18n/LangContext'; +import * as connectionsService from '../../../services/connectionsService'; +import ClientsPage from '../clients'; + +vi.mock('../../../services/connectionsService', () => ({ + listConnections: vi.fn(), +})); + +const connection: ClientConnection = { + clientId: 'order-svc-0@10.0.1.12:49152', + type: 'Producer', + groupOrTopic: 'order-create', + protocol: 'gRPC', + address: '10.0.1.12:49152', + language: 'Java', + version: '5.0.7', + connectedAt: '2026-07-01 08:30:00', + clusterName: 'ns-prod', +}; + +beforeAll(() => { + Object.defineProperty(window, 'matchMedia', { + writable: true, + value: vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })), + }); +}); + +beforeEach(() => { + vi.mocked(connectionsService.listConnections).mockResolvedValue([connection]); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +const renderWithProviders = (ui: React.ReactElement) => + render( + + {ui} + , + ); + +describe('Clients page', () => { + it('opens a client detail dialog from the connection table', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + const row = await screen.findByRole('row', { name: /order-svc-0@10\.0\.1\.12:49152/ }); + await user.click(within(row).getByRole('button', { name: /详情/ })); + + const dialog = await screen.findByRole('dialog', { + name: /客户端详情 - order-svc-0@10\.0\.1\.12:49152/, + }); + expect(within(dialog).getAllByText('order-svc-0@10.0.1.12:49152')).toHaveLength(1); + expect(within(dialog).getByText('ns-prod')).toBeInTheDocument(); + expect(within(dialog).getByText('Producer')).toBeInTheDocument(); + expect(within(dialog).getByText('order-create')).toBeInTheDocument(); + expect(within(dialog).getByText('gRPC')).toBeInTheDocument(); + expect(within(dialog).getByText('10.0.1.12:49152')).toBeInTheDocument(); + expect(within(dialog).getByText('Java')).toBeInTheDocument(); + expect(within(dialog).getByText('5.0.7')).toBeInTheDocument(); + expect(within(dialog).getByText('2026-07-01 08:30:00')).toBeInTheDocument(); + }); +}); diff --git a/web/src/pages/cluster/clients.tsx b/web/src/pages/cluster/clients.tsx index 8cca2c23..8071e5fa 100644 --- a/web/src/pages/cluster/clients.tsx +++ b/web/src/pages/cluster/clients.tsx @@ -16,8 +16,21 @@ */ import { useEffect, useMemo, useState } from 'react'; -import { Table, Card, Tag, Space, Input, Select, Flex, Typography, message } from 'antd'; -import { MagnifyingGlass } from '@phosphor-icons/react'; +import { + Button, + Card, + Descriptions, + Flex, + Input, + Modal, + Select, + Space, + Table, + Tag, + Typography, + message, +} from 'antd'; +import { Eye, MagnifyingGlass } from '@phosphor-icons/react'; import type { ColumnsType } from 'antd/es/table'; import PageHeader from '../../components/PageHeader'; @@ -55,6 +68,7 @@ const ClientsPage = () => { const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [clusterFilter, setClusterFilter] = useState('ALL'); + const [selectedConnection, setSelectedConnection] = useState(null); useEffect(() => { let cancelled = false; @@ -221,6 +235,22 @@ const ClientsPage = () => { ), }, + { + title: t('common.actions'), + key: 'actions', + width: 90, + fixed: 'right', + render: (_: unknown, record: ClientConnection) => ( + + ), + }, ]; /* ═══════════════════════════════════════════ @@ -271,6 +301,53 @@ const ClientsPage = () => { size="small" /> + + setSelectedConnection(null)} + footer={} + width={640} + destroyOnClose + > + {selectedConnection && ( + + + + {selectedConnection.clientId} + + + + {selectedConnection.clusterName} + + + {typeConfig[selectedConnection.type]?.label ?? selectedConnection.type} + + + {selectedConnection.groupOrTopic} + + + + {protocolConfig[selectedConnection.protocol]?.label ?? selectedConnection.protocol} + + + + {selectedConnection.address} + + + + {languageConfig[selectedConnection.language]?.label ?? selectedConnection.language} + + + + {selectedConnection.version} + + + {selectedConnection.connectedAt} + + + )} + ); };