From 9859be61773d00be4c6b8b749dd2d61f5afc8fa5 Mon Sep 17 00:00:00 2001 From: liuhy Date: Sat, 25 Jul 2026 07:52:18 -0700 Subject: [PATCH] Complete DLQ detail and export actions --- .../pages/instance/__tests__/DLQPage.test.tsx | 124 ++++++++++++++++++ web/src/pages/instance/dlq.tsx | 93 ++++++++++++- 2 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 web/src/pages/instance/__tests__/DLQPage.test.tsx diff --git a/web/src/pages/instance/__tests__/DLQPage.test.tsx b/web/src/pages/instance/__tests__/DLQPage.test.tsx new file mode 100644 index 00000000..b1054073 --- /dev/null +++ b/web/src/pages/instance/__tests__/DLQPage.test.tsx @@ -0,0 +1,124 @@ +/* + * 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 } 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 { DLQGroup } from '../../../api/message'; +import { LangProvider } from '../../../i18n/LangContext'; +import * as messageService from '../../../services/messageService'; +import DLQPage from '../dlq'; + +vi.mock('../../../services/messageService', () => ({ + listDLQGroups: vi.fn(), + resendDLQ: vi.fn(), +})); + +const dlqGroup: DLQGroup = { + groupName: 'cg-order', + dlqTopic: '%DLQ%cg-order', + messageCount: 7, + lastEnqueueTime: '2026-07-24T10:00:00Z', + retryCount: 3, + status: 'ACTIVE', +}; + +const renderWithProviders = (ui: React.ReactElement) => + render( + + {ui} + , + ); + +describe('DLQ page', () => { + let createObjectURL: ReturnType; + let revokeObjectURL: ReturnType; + let clickSpy: ReturnType; + + 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(() => { + createObjectURL = vi.fn().mockReturnValue('blob:dlq'); + revokeObjectURL = vi.fn(); + Object.defineProperty(URL, 'createObjectURL', { + configurable: true, + value: createObjectURL, + }); + Object.defineProperty(URL, 'revokeObjectURL', { + configurable: true, + value: revokeObjectURL, + }); + clickSpy = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => {}); + vi.mocked(messageService.listDLQGroups).mockResolvedValue([dlqGroup]); + }); + + afterEach(() => { + clickSpy.mockRestore(); + vi.clearAllMocks(); + }); + + it('loads DLQ groups through the service layer', async () => { + renderWithProviders(); + + expect(await screen.findByText('cg-order')).toBeInTheDocument(); + expect(screen.getByText('%DLQ%cg-order')).toBeInTheDocument(); + expect(messageService.listDLQGroups).toHaveBeenCalledTimes(1); + }); + + it('opens a detail dialog with the selected group metadata', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await screen.findByText('cg-order'); + await user.click(screen.getByRole('button', { name: /查看详情/ })); + + expect(await screen.findByText('死信队列详情')).toBeInTheDocument(); + expect(screen.getAllByText('cg-order')).toHaveLength(2); + expect(screen.getAllByText('%DLQ%cg-order')).toHaveLength(2); + expect(screen.getByText('ACTIVE')).toBeInTheDocument(); + }); + + it('exports the selected group summary as CSV', async () => { + const user = userEvent.setup(); + renderWithProviders(); + + await screen.findByText('cg-order'); + await user.click(screen.getByRole('button', { name: /导出/ })); + + expect(createObjectURL).toHaveBeenCalledTimes(1); + const blob = createObjectURL.mock.calls[0][0] as Blob; + await expect(blob.text()).resolves.toContain('"cg-order","%DLQ%cg-order","7","3","ACTIVE"'); + expect(clickSpy).toHaveBeenCalledTimes(1); + expect(revokeObjectURL).toHaveBeenCalledWith('blob:dlq'); + }); +}); diff --git a/web/src/pages/instance/dlq.tsx b/web/src/pages/instance/dlq.tsx index 88a385d6..afe486a5 100644 --- a/web/src/pages/instance/dlq.tsx +++ b/web/src/pages/instance/dlq.tsx @@ -65,6 +65,7 @@ const DLQPage = () => { ]); const [retryTargetTopic, setRetryTargetTopic] = useState(''); const [retrySubmitting, setRetrySubmitting] = useState(false); + const [detailGroup, setDetailGroup] = useState(null); useEffect(() => { let cancelled = false; @@ -129,7 +130,32 @@ const DLQPage = () => { }; const handleExport = (group: DLQGroup) => { - message.success(`已导出 ${group.groupName} 的死信消息(模拟)`); + const rows = [ + ['Group Name', 'DLQ Topic', 'Message Count', 'Retry Count', 'Status', 'Last Enqueue Time'], + [ + group.groupName, + group.dlqTopic, + String(group.messageCount), + String(group.retryCount), + group.status, + group.lastEnqueueTime, + ], + ]; + const csv = rows + .map((row) => + row + .map((value) => `"${value.replace(/"/g, '""')}"`) + .join(','), + ) + .join('\n'); + const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${group.groupName}-dlq.csv`; + a.click(); + URL.revokeObjectURL(url); + message.success(`已导出 ${group.groupName} 的死信队列摘要`); }; /* ─── Table Columns ─── */ @@ -196,7 +222,7 @@ const DLQPage = () => { size="small" icon={} style={{ borderColor: '#1677ff', color: '#1677ff' }} - onClick={() => message.info(`查看 ${record.groupName} 详情(模拟)`)} + onClick={() => setDetailGroup(record)} > 查看详情 @@ -345,6 +371,69 @@ const DLQPage = () => { )} + + setDetailGroup(null)} + footer={} + width={560} + destroyOnClose + > + {detailGroup && ( +
+
+ + Group 名称 + + + {detailGroup.groupName} + +
+
+ + DLQ Topic + + + {detailGroup.dlqTopic} + +
+ +
+ + 死信数量 + + 0 ? '#fa8c16' : undefined }} + > + {detailGroup.messageCount.toLocaleString()} + +
+
+ + 重试次数 + + {detailGroup.retryCount.toLocaleString()} +
+
+ + 状态 + + {detailGroup.status} +
+
+
+ + 最近入队时间 + + + {formatDateTime(detailGroup.lastEnqueueTime)} + +
+
+ )} +
); };