From 245ba57c646258c2d9a12d230ec51f564acd3527 Mon Sep 17 00:00:00 2001 From: Linzp Date: Mon, 11 May 2026 18:08:28 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=B6=88=E6=81=AF=E9=98=9F?= =?UTF-8?q?=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- prompts/README.md | 173 +++++++++++++ src/components/Apis/getApis.js | 52 ++++ .../MessageQueue/Actions/DeadLetterActions.js | 42 ++++ .../MessageQueue/Actions/DeadLetterReplay.js | 43 ++++ .../MessageQueue/Actions/MessageDetail.js | 48 ++++ src/components/MessageQueue/Actions/index.js | 39 +++ .../MessageQueue/Dashboard/index.js | 190 ++++++++++++++ .../MessageQueue/DeadLetterList/index.js | 120 +++++++++ src/components/MessageQueue/Menu.js | 27 ++ .../MessageQueue/MessageList/index.js | 98 ++++++++ .../MessageQueue/PublishMessage/index.js | 108 ++++++++ .../MessageQueue/QueueTools/index.js | 92 +++++++ src/components/MessageQueue/README.md | 232 ++++++++++++++++++ .../MessageQueue/TraceList/index.js | 91 +++++++ src/components/MessageQueue/doc/api.md | 67 +++++ src/components/MessageQueue/doc/base.js | 37 +++ .../MessageQueue/doc/dead-letter-replay.js | 23 ++ src/components/MessageQueue/doc/enums.js | 33 +++ src/components/MessageQueue/doc/example.json | 50 ++++ .../MessageQueue/doc/message-detail.js | 23 ++ src/components/MessageQueue/doc/style.scss | 0 src/components/MessageQueue/doc/summary.md | 5 + src/components/MessageQueue/enums.js | 29 +++ src/components/MessageQueue/getColumns.js | 99 ++++++++ .../MessageQueue/getDeadLetterColumns.js | 69 ++++++ .../MessageQueue/getTraceColumns.js | 64 +++++ src/components/MessageQueue/index.js | 49 ++++ src/components/MessageQueue/locale/en-US.js | 80 ++++++ src/components/MessageQueue/locale/zh-CN.js | 80 ++++++ src/components/MessageQueue/style.module.scss | 0 src/components/MessageQueue/utils.js | 151 ++++++++++++ src/components/MessageQueue/utils.test.js | 88 +++++++ src/components/MessageQueue/withLocale.js | 14 ++ src/mockPreset/dead-letter-list.json | 60 +++++ src/mockPreset/index.js | 166 ++++++++++++- src/mockPreset/message-queue-list.json | 141 +++++++++++ src/mockPreset/trace-list.json | 86 +++++++ src/preset.js | 2 +- 39 files changed, 2769 insertions(+), 4 deletions(-) create mode 100644 prompts/README.md create mode 100644 src/components/MessageQueue/Actions/DeadLetterActions.js create mode 100644 src/components/MessageQueue/Actions/DeadLetterReplay.js create mode 100644 src/components/MessageQueue/Actions/MessageDetail.js create mode 100644 src/components/MessageQueue/Actions/index.js create mode 100644 src/components/MessageQueue/Dashboard/index.js create mode 100644 src/components/MessageQueue/DeadLetterList/index.js create mode 100644 src/components/MessageQueue/Menu.js create mode 100644 src/components/MessageQueue/MessageList/index.js create mode 100644 src/components/MessageQueue/PublishMessage/index.js create mode 100644 src/components/MessageQueue/QueueTools/index.js create mode 100644 src/components/MessageQueue/README.md create mode 100644 src/components/MessageQueue/TraceList/index.js create mode 100644 src/components/MessageQueue/doc/api.md create mode 100644 src/components/MessageQueue/doc/base.js create mode 100644 src/components/MessageQueue/doc/dead-letter-replay.js create mode 100644 src/components/MessageQueue/doc/enums.js create mode 100644 src/components/MessageQueue/doc/example.json create mode 100644 src/components/MessageQueue/doc/message-detail.js create mode 100644 src/components/MessageQueue/doc/style.scss create mode 100644 src/components/MessageQueue/doc/summary.md create mode 100644 src/components/MessageQueue/enums.js create mode 100644 src/components/MessageQueue/getColumns.js create mode 100644 src/components/MessageQueue/getDeadLetterColumns.js create mode 100644 src/components/MessageQueue/getTraceColumns.js create mode 100644 src/components/MessageQueue/index.js create mode 100644 src/components/MessageQueue/locale/en-US.js create mode 100644 src/components/MessageQueue/locale/zh-CN.js create mode 100644 src/components/MessageQueue/style.module.scss create mode 100644 src/components/MessageQueue/utils.js create mode 100644 src/components/MessageQueue/utils.test.js create mode 100644 src/components/MessageQueue/withLocale.js create mode 100644 src/mockPreset/dead-letter-list.json create mode 100644 src/mockPreset/message-queue-list.json create mode 100644 src/mockPreset/trace-list.json diff --git a/package.json b/package.json index 6490fa2..26bd975 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@kne-components/components-admin", - "version": "1.1.34", + "version": "1.1.35", "description": "用于实现一个后台管理系统的必要组件", "scripts": { "init": "husky", diff --git a/prompts/README.md b/prompts/README.md new file mode 100644 index 0000000..979f126 --- /dev/null +++ b/prompts/README.md @@ -0,0 +1,173 @@ +# Prompts 文档集合 + +本项目包含多个 AI prompts 文档,用于指导生成前端组件库相关的代码模块、文档和示例。 + +--- + +## 文档集合列表 + +### 1. BizUnit 使用指南 + +**功能**: 生成基于 BizUnit 架构模式的完整前端业务模块 + +**适用场景**: +- 需要生成包含完整 CRUD 功能的前端业务模块 +- 需要国际化支持和可复用组件结构 +- 生成符合规范的目录结构和文档示例 + +**核心内容**: +- 模块目录结构规范(List、Detail、FormInner、TabDetail、Actions 等组件) +- 核心组件实现规范(根组件、列表页、表单组件、详情页、Tab 详情页) +- 国际化文件规范(中英文语言包) +- API 集成规范 +- 文档示例规范 + +**所属集合**: `prompts-remote-components/` + +--- + +### 2. RemoteLoader 使用指南 + +**功能**: 远程模块加载库的使用指南,基于 Webpack 5 Module Federation + +**适用场景**: +- 构建微前端架构 +- 需要在运行时动态加载远程模块 +- 多团队独立开发部署模块的场景 + +**核心内容**: +- 四种使用方式:RemoteLoader 组件、withRemoteLoader HOC、useLoader Hook、createWithRemoteLoader +- 模块标记格式详解 +- API 参考(preset、loadModule、safeLoadModule、parseToken 等) +- 缓存机制 +- 错误处理和调试 +- 性能优化 + +**所属集合**: `prompts-remote-components/` + +--- + +### 3. FormInfo 使用指南 + +**功能**: 基于 React 和 Ant Design 的企业级表单组件库 + +**适用场景**: +- 构建复杂的表单页面 +- 需要表单验证、动态字段、弹窗/抽屉表单 +- 分步表单向导 + +**核心内容**: +- 核心组件:Form、FormInfo、SubmitButton、CancelButton +- 字段类型:Input、TextArea、Select、DatePicker、Upload 等 +- 校验规则配置 +- 列表组件:List(卡片式)、TableList(表格) +- 弹窗与抽屉:FormModal、FormDrawer +- 分步表单:FormSteps、FormStepModal +- 表单 Hook:useFormModal、useFormDrawer、useFormStepModal +- 国际化支持 + +**所属集合**: 根目录、`prompts-remote-components/`(内容相同) + +--- + +### 4. 国际化 + +**功能**: 指导组件完成国际化改造 + +**适用场景**: +- 需要为组件添加多语言支持 +- 创建语言包和国际化上下文 +- 修改组件以支持国际化 + +**核心内容**: +- 国际化文件创建(withLocale.js、locale/zh-CN.js、locale/en-US.js) +- 组件修改模式(主组件、FormInner、getColumns、Action) +- useIntl Hook 和 withLocale HOC 使用方式 +- createWithRemoteLoader 组件的国际化包裹规范 +- 语言包 key 命名规范 +- 检查要点清单 + +**所属集合**: `prompts-remote-components/` + +--- + +### 5. 生成文档 + +**功能**: 根据代码实现自动生成项目文档(summary.md 和 api.md) + +**适用场景**: +- 需要为组件生成规范化的项目概述文档 +- 需要生成 API 属性表格文档 +- 组件开发完成后需要补充文档 + +**核心内容**: +- 项目概述文档(doc/summary.md)格式规范 +- API 文档(doc/api.md)格式规范 +- 文档生成流程(分析代码结构、提取 API 信息) +- 格式约束(标题级别、表格格式、无示例代码) + +**所属集合**: `prompts-remote-components/` + +--- + +### 6. 组件示例编写提示词 + +**功能**: 指导编写规范的组件示例代码和配置 + +**适用场景**: +- 为组件编写可运行的示例代码 +- 配置 example.json 示例配置文件 +- 编写覆盖 API 的完整示例 + +**核心内容**: +- 文件结构规范(doc/ 目录、子组件示例规则) +- example.json 配置结构 +- 示例代码规范(scope 依赖声明、导入方式) +- 示例内容设计原则(API 覆盖率、真实业务场景、数据真实性) +- FormInfo 组件示例特殊规则 +- Mock 数据规范和使用方式 +- 示例完整性检查清单 + +**所属集合**: 根目录、`prompts-remote-components/`(内容略有差异) + +--- + +### 7. BizUnit 业务模块生成提示词 + +**功能**: 快速生成业务模块 action 的代码模板 + +**适用场景**: +- 需要快速生成业务表单操作按钮 +- 创建新建、编辑类业务组件 + +**核心内容**: +- 组件命名规范(大写字母开头英文名称) +- 代码模板结构 +- 成功提示语配置 +- 表单弹窗集成 + +**所属集合**: 根目录 + +--- + +## 快速选择指南 + +| 需求 | 推荐文档 | 所属集合 | +|------|---------|---------| +| 生成完整的业务模块(列表+表单+详情) | BizUnit 使用指南 | prompts-remote-components/ | +| 加载远程组件/微前端 | RemoteLoader 使用指南 | prompts-remote-components/ | +| 构建表单页面(验证、动态字段、弹窗) | FormInfo 使用指南 | 两者都有 | +| 为组件添加多语言支持 | 国际化 | prompts-remote-components/ | +| 为组件生成项目概述和 API 文档 | 生成文档 | prompts-remote-components/ | +| 编写组件示例代码和配置 | 组件示例编写提示词 | 两者都有 | +| 快速生成业务 Action 组件 | BizUnit 业务模块生成提示词 | 根目录 | + +--- + +## 版本信息 + +```json +{ + "@kne/prompts-remote-components": "1.0.2" +} +``` diff --git a/src/components/Apis/getApis.js b/src/components/Apis/getApis.js index aefb7c6..8c99da5 100644 --- a/src/components/Apis/getApis.js +++ b/src/components/Apis/getApis.js @@ -481,6 +481,58 @@ const getApis = options => { method: 'POST' } } + }, + mq: { + message: { + publish: { + url: `${prefix}/mq/message/publish`, + method: 'POST' + }, + list: { + url: `${prefix}/mq/message/list`, + method: 'GET' + } + }, + deadLetter: { + list: { + url: `${prefix}/mq/dlq/list`, + method: 'GET' + }, + replay: { + url: `${prefix}/mq/dlq/replay`, + method: 'POST' + } + }, + trace: { + list: { + url: `${prefix}/mq/trace/list`, + method: 'GET' + }, + detail: { + url: `${prefix}/mq/trace/detail`, + method: 'GET' + } + }, + dashboard: { + getData: { + url: `${prefix}/mq/dashboard`, + method: 'GET' + }, + sse: { + url: `${prefix}/mq/dashboard/sse`, + method: 'GET' + } + }, + queue: { + depth: { + url: `${prefix}/mq/queue/depth`, + method: 'GET' + }, + cleanup: { + url: `${prefix}/mq/queue/cleanup`, + method: 'POST' + } + } } }; }; diff --git a/src/components/MessageQueue/Actions/DeadLetterActions.js b/src/components/MessageQueue/Actions/DeadLetterActions.js new file mode 100644 index 0000000..839d341 --- /dev/null +++ b/src/components/MessageQueue/Actions/DeadLetterActions.js @@ -0,0 +1,42 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; +import DeadLetterReplay from './DeadLetterReplay'; +import MessageDetail from './MessageDetail'; + +const DeadLetterActionsInner = createWithRemoteLoader({ + modules: ['components-core:ButtonGroup'] +})(({ remoteModules, children, data, onSuccess, moreType = 'link', itemClassName, ...props }) => { + const [ButtonGroup] = remoteModules; + const { formatMessage } = useIntl(); + const list = []; + + if (data && !data.replayed) { + list.push({ + ...props, + buttonComponent: DeadLetterReplay, + data, + children: formatMessage({ id: 'Replay' }), + onSuccess + }); + } + + if (data) { + list.push({ + ...props, + buttonComponent: MessageDetail, + data, + title: formatMessage({ id: 'DeadLetterList' }), + children: formatMessage({ id: 'ViewDetail' }) + }); + } + + if (typeof children === 'function') { + return children({ list }); + } + + return ; +}); + +const DeadLetterActions = withLocale(DeadLetterActionsInner); +export default DeadLetterActions; diff --git a/src/components/MessageQueue/Actions/DeadLetterReplay.js b/src/components/MessageQueue/Actions/DeadLetterReplay.js new file mode 100644 index 0000000..47d10c4 --- /dev/null +++ b/src/components/MessageQueue/Actions/DeadLetterReplay.js @@ -0,0 +1,43 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { App } from 'antd'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +const DeadLetterReplay = createWithRemoteLoader({ + modules: ['components-core:Global@usePreset', 'components-core:ConfirmButton'] +})( + withLocale(({ remoteModules, data, ids, onSuccess, message: propsMessage, ...props }) => { + const [usePreset, ConfirmButton] = remoteModules; + const { ajax, apis } = usePreset(); + const { message } = App.useApp(); + const { formatMessage } = useIntl(); + const replayIds = ids || (data?.id ? [data.id] : []); + + if (data?.replayed) { + return null; + } + + return ( + 1 ? formatMessage({ id: 'BatchReplayConfirm' }) : formatMessage({ id: 'ReplayMessageConfirm' }))} + onClick={async () => { + const { data: resData } = await ajax( + Object.assign({}, apis.mq.deadLetter.replay, { + data: replayIds.length > 1 ? { ids: replayIds } : { id: replayIds[0] } + }) + ); + if (resData.code !== 0) { + return; + } + message.success(formatMessage({ id: 'ReplaySuccess' })); + onSuccess && onSuccess(); + }} + /> + ); + }) +); + +export default DeadLetterReplay; diff --git a/src/components/MessageQueue/Actions/MessageDetail.js b/src/components/MessageQueue/Actions/MessageDetail.js new file mode 100644 index 0000000..2ddfb8f --- /dev/null +++ b/src/components/MessageQueue/Actions/MessageDetail.js @@ -0,0 +1,48 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { Button, Descriptions } from 'antd'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; +import JsonView from '@kne/json-view'; +import '@kne/json-view/dist/index.css'; + +const MessageDetail = createWithRemoteLoader({ + modules: ['components-core:Modal@useModal'] +})( + withLocale(({ remoteModules, data, title, ...props }) => { + const [useModal] = remoteModules; + const modal = useModal(); + const { formatMessage } = useIntl(); + + return ( + + + } + children={ + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + `${formatRate(value)}/s` }, + { title: formatMessage({ id: 'FailureRate' }), dataIndex: 'failureRate', render: value => `${formatRate(value)}/s` }, + { title: formatMessage({ id: 'SuccessRatio' }), dataIndex: 'successRatio', render: formatPercent } + ]} + /> + + } + /> + ); +}); + +const Dashboard = createWithRemoteLoader({ + modules: ['components-core:Global@usePreset', 'components-core:Layout@Page'] +})( + withLocale(({ remoteModules, baseUrl, pageProps = {} }) => { + const [usePreset, Page] = remoteModules; + const { apis } = usePreset(); + + return ( + { + return ; + }} + /> + ); + }) +); + +export default Dashboard; diff --git a/src/components/MessageQueue/DeadLetterList/index.js b/src/components/MessageQueue/DeadLetterList/index.js new file mode 100644 index 0000000..26829a9 --- /dev/null +++ b/src/components/MessageQueue/DeadLetterList/index.js @@ -0,0 +1,120 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { useRef, useState } from 'react'; +import { Space } from 'antd'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +import getDeadLetterColumns from '../getDeadLetterColumns'; +import DeadLetterActions from '../Actions/DeadLetterActions'; +import DeadLetterReplay from '../Actions/DeadLetterReplay'; +import Menu from '../Menu'; +import { buildListParams } from '../utils'; + +const DeadLetterList = createWithRemoteLoader({ + modules: ['components-core:Layout@TablePage', 'components-core:Global@usePreset', 'components-core:Filter'] +})( + withLocale(({ remoteModules, baseUrl, pageProps = {} }) => { + const [TablePage, usePreset, Filter] = remoteModules; + const { formatMessage } = useIntl(); + const { apis } = usePreset(); + const { SearchInput, getFilterValue, fields: filterFields } = Filter; + const { InputFilterItem, AdvancedSelectFilterItem } = filterFields; + const ref = useRef(null); + const [filter, setFilter] = useState([]); + const [selected, setSelected] = useState({ + selectedRowKeys: [], + selectedRows: [] + }); + const filterValue = getFilterValue(filter); + + return ( + { + return { + children: ( + { + ref.current?.reload?.(); + }} + /> + ) + }; + } + } + ]} + rowSelection={{ + type: 'checkbox', + selectedRowKeys: selected.selectedRowKeys, + onChange: (selectedRowKeys, selectedRows) => { + setSelected({ selectedRowKeys, selectedRows }); + }, + getCheckboxProps: record => { + return { + disabled: record.replayed + }; + } + }} + topArea={ + + {formatMessage({ id: 'SelectedCount' }, { count: selected.selectedRowKeys.length })} + { + setSelected({ selectedRowKeys: [], selectedRows: [] }); + ref.current?.reload?.(); + }}> + {formatMessage({ id: 'BatchReplay' })} + + + } + page={{ + filter: { + value: filter, + onChange: setFilter, + list: [ + [ + , + ({ + pageData: [ + { label: formatMessage({ id: 'Yes' }), value: true }, + { label: formatMessage({ id: 'No' }), value: false } + ] + }) + }} + /> + ] + ] + }, + titleExtra: , + menu: , + ...pageProps + }} + /> + ); + }) +); + +export default DeadLetterList; diff --git a/src/components/MessageQueue/Menu.js b/src/components/MessageQueue/Menu.js new file mode 100644 index 0000000..b33c421 --- /dev/null +++ b/src/components/MessageQueue/Menu.js @@ -0,0 +1,27 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import withLocale from './withLocale'; +import { useIntl } from '@kne/react-intl'; + +const Menu = createWithRemoteLoader({ + modules: ['components-core:Menu'] +})( + withLocale(({ remoteModules, baseUrl }) => { + const [Menu] = remoteModules; + const { formatMessage } = useIntl(); + const rootPath = baseUrl || ''; + + return ( + + ); + }) +); + +export default Menu; diff --git a/src/components/MessageQueue/MessageList/index.js b/src/components/MessageQueue/MessageList/index.js new file mode 100644 index 0000000..df5891a --- /dev/null +++ b/src/components/MessageQueue/MessageList/index.js @@ -0,0 +1,98 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { useRef, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Space } from 'antd'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +import getColumns from '../getColumns'; +import Actions from '../Actions'; +import Menu from '../Menu'; +import PublishMessage from '../PublishMessage'; +import { buildListParams } from '../utils'; + +const MessageList = createWithRemoteLoader({ + modules: ['components-core:Layout@TablePage', 'components-core:Global@usePreset', 'components-core:Filter', 'components-core:Enum'] +})( + withLocale(({ remoteModules, baseUrl, pageProps = {} }) => { + const [TablePage, usePreset, Filter, Enum] = remoteModules; + const { formatMessage } = useIntl(); + const { apis } = usePreset(); + const { SearchInput, getFilterValue, fields: filterFields } = Filter; + const { InputFilterItem, SuperSelectFilterItem } = filterFields; + const navigate = useNavigate(); + const ref = useRef(null); + const [filter, setFilter] = useState([]); + const filterValue = getFilterValue(filter); + + return ( + { + return { + children: ( + { + navigate(`${baseUrl}/traces?messageId=${encodeURIComponent(item.messageId || item.id)}`); + }} + onSuccess={() => { + ref.current?.reload?.(); + }} + /> + ) + }; + } + } + ]} + page={{ + filter: { + value: filter, + onChange: setFilter, + list: [ + [ + , + , + { + return ( + + {options => children({ options })} + + ); + }} + /> + ] + ] + }, + titleExtra: ( + + + ref.current?.reload?.()} /> + + ), + menu: , + ...pageProps + }} + /> + ); + }) +); + +export default MessageList; diff --git a/src/components/MessageQueue/PublishMessage/index.js b/src/components/MessageQueue/PublishMessage/index.js new file mode 100644 index 0000000..bc674c2 --- /dev/null +++ b/src/components/MessageQueue/PublishMessage/index.js @@ -0,0 +1,108 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { App, Button } from 'antd'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; +import { parseIsoDateTimeInput, parseJsonInput, parseNumberInput } from '../utils'; + +const PublishMessage = createWithRemoteLoader({ + modules: [ + 'components-core:FormInfo', + 'components-core:FormInfo@useFormModal', + 'components-core:Global@usePreset', + 'components-thirdparty:JSONEditor' + ] +})( + withLocale(({ remoteModules, onSuccess, ...props }) => { + const [FormInfo, useFormModal, usePreset, JSONEditor] = remoteModules; + const { Input, InputNumber,DatePicker } = FormInfo.fields; + const formModal = useFormModal(); + const { ajax, apis } = usePreset(); + const { message } = App.useApp(); + const { formatMessage } = useIntl(); + + return ( + + ); + }) +); + +export default PublishMessage; diff --git a/src/components/MessageQueue/QueueTools/index.js b/src/components/MessageQueue/QueueTools/index.js new file mode 100644 index 0000000..bc7b53f --- /dev/null +++ b/src/components/MessageQueue/QueueTools/index.js @@ -0,0 +1,92 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { App, Button, Card, Input, Select, Space, Statistic } from 'antd'; +import { useState } from 'react'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; +import Menu from '../Menu'; + +const QueueTools = createWithRemoteLoader({ + modules: ['components-core:Global@usePreset', 'components-core:Layout@Page', 'components-core:ConfirmButton'] +})( + withLocale(({ remoteModules, baseUrl, pageProps = {} }) => { + const [usePreset, Page, ConfirmButton] = remoteModules; + const { ajax, apis } = usePreset(); + const { message } = App.useApp(); + const { formatMessage } = useIntl(); + const [topic, setTopic] = useState(''); + const [depth, setDepth] = useState(null); + const [cleanupStatus, setCleanupStatus] = useState('COMPLETED'); + const [olderThan, setOlderThan] = useState(''); + + const queryDepth = async () => { + const { data: resData } = await ajax( + Object.assign({}, apis.mq.queue.depth, { + params: topic ? { topic } : {} + }) + ); + if (resData.code !== 0) { + return; + } + setDepth(resData.data?.depth || 0); + }; + + return ( + } + children={ + + + + setTopic(event.target.value)} placeholder={formatMessage({ id: 'TopicPlaceholder' })} style={{ width: 280 }} /> + + + {depth !== null && } + + + + setOlderThan(event.target.value)} placeholder={`${formatMessage({ id: 'OlderThan' })}: 2026-05-01T00:00:00.000Z`} style={{ width: 320 }} /> + { + const { data: resData } = await ajax( + Object.assign({}, apis.mq.queue.cleanup, { + data: Object.assign( + { + status: cleanupStatus + }, + olderThan ? { olderThan } : {} + ) + }) + ); + if (resData.code !== 0) { + return; + } + message.success(formatMessage({ id: 'CleanupSuccess' })); + }}> + {formatMessage({ id: 'CleanupMessages' })} + + + + + } + /> + ); + }) +); + +export default QueueTools; diff --git a/src/components/MessageQueue/README.md b/src/components/MessageQueue/README.md new file mode 100644 index 0000000..3425034 --- /dev/null +++ b/src/components/MessageQueue/README.md @@ -0,0 +1,232 @@ +# MessageQueue + +### 概述 + +MessageQueue 是面向 `fastify-mq` 的消息队列管理端组件,提供队列运行概览、消息发布与查询、死信处理、轨迹追踪和队列维护工具。 + +组件直接对齐 `fastify-mq` 的接口契约,列表筛选使用后端支持的扁平查询参数,死信支持单条与批量重放,Dashboard 展示队列深度、消费速率、失败率、死信速率和成功率等关键指标。 + +适用于需要在后台管理系统中观察异步消息处理状态、排查失败消息、追踪消息生命周期,以及执行基础队列运维操作的业务场景。 + + +### 示例(全屏) + +#### 示例代码 + +- 基础用法 +- 完整展示 MessageQueue 的仪表盘、消息列表、死信队列、轨迹追踪和队列工具页。 +- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),reactRouterDom(react-router-dom),antd(antd) + +```jsx +const { default: MessageQueue } = _MessageQueue; +const { default: mockPreset } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { useNavigate, Navigate, Route, Routes } = reactRouterDom; +const { Button, Flex } = antd; + +const BaseExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const navigate = useNavigate(); + return ( + + + + + + + + + + + + + } + /> + } /> + + + + ); +}); + +render(); + +``` + +- 消息详情 +- 展示独立使用消息操作按钮查看消息详情。 +- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),antd(antd) + +```jsx +const { Actions } = _MessageQueue; +const { default: mockPreset, messageQueueList } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { Card, Space } = antd; + +const MessageDetailExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const data = messageQueueList.pageData[0]; + return ( + + + + {data.topic} + + + + + ); +}); + +render(); + +``` + +- 死信重放 +- 展示独立使用死信操作按钮提交重放。 +- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),antd(antd) + +```jsx +const { DeadLetterActions } = _MessageQueue; +const { default: mockPreset, deadLetterList } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { Card, Space } = antd; + +const DeadLetterReplayExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const data = deadLetterList.pageData.find(item => !item.replayed); + return ( + + + + {data.topic} + + + + + ); +}); + +render(); + +``` + +- 枚举值 +- 展示消息状态和轨迹事件枚举。 +- _MessageQueue(@components/MessageQueue),_mockPreset(@root/mockPreset),remoteLoader(@kne/remote-loader),antd(antd) + +```jsx +const { enums } = _MessageQueue; +const { default: mockPreset } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { Card, Space, Tag } = antd; + +const EnumsExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const getColor = type => ({ success: 'green', danger: 'red', progress: 'blue', info: 'default' })[type] || 'default'; + return ( + + + + + {enums.messageStatus.map(item => ( + {item.value} - {item.description} + ))} + + + + + {enums.traceEvent.map(item => ( + {item.value} - {item.description} + ))} + + + + + ); +}); + +render(); + +``` + +### API + +### MessageQueue + +消息队列管理主组件,内部包含 Dashboard、消息列表、死信列表、轨迹列表和队列工具页。 + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| baseUrl | 组件挂载的基础路由,例如 `/MessageQueue/mq` | string | - | +| pageProps | 传给内部页面组件的布局配置 | object | `{}` | +| children | 自定义顶部导航或附加内容 | ReactNode | - | + +### 子组件 + +| 导出名 | 说明 | +| --- | --- | +| Dashboard | 队列指标概览页 | +| MessageList | 消息列表页,支持发布消息和查看详情 | +| DeadLetterList | 死信列表页,支持单条和批量重放 | +| TraceList | 消息轨迹列表页 | +| QueueTools | 队列深度查询和消息清理工具页 | +| PublishMessage | 发布消息按钮组件 | +| Actions | 消息列表操作按钮 | +| DeadLetterActions | 死信列表操作按钮 | + +### Actions + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| data | 消息记录 | object | - | +| onTrace | 点击查看轨迹时触发 | `(data) => void` | - | +| onSuccess | 操作成功后的回调 | `() => void` | - | +| type | 按钮类型 | string | `default` | + +### DeadLetterActions + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| data | 死信记录 | object | - | +| onSuccess | 重放成功后的回调 | `() => void` | - | +| type | 按钮类型 | string | `default` | + +### PublishMessage + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| onSuccess | 发布成功后的回调 | `() => void` | - | + +### 枚举 + +| 枚举名 | 值 | +| --- | --- | +| messageStatus | `PENDING`、`PROCESSING`、`COMPLETED`、`FAILED` | +| traceEvent | `PUBLISHED`、`PROCESSING`、`COMPLETED`、`FAILED`、`MOVED_TO_DLQ`、`REPLAYED`、`LOCK_RECOVERED` | +| mqBoolean | `true`、`false` | + +### fastify-mq 接口 + +| 功能 | 方法 | 路径 | 参数 | +| --- | --- | --- | --- | +| 发布消息 | POST | `/mq/message/publish` | `topic`、`payload`、`priority`、`executeAt`、`maxRetries`、`traceId`、`meta` | +| 消息列表 | GET | `/mq/message/list` | `topic`、`status`、`traceId`、`perPage`、`currentPage` | +| 死信列表 | GET | `/mq/dlq/list` | `topic`、`replayed`、`perPage`、`currentPage` | +| 重放死信 | POST | `/mq/dlq/replay` | `id` 或 `ids` | +| 轨迹列表 | GET | `/mq/trace/list` | `topic`、`messageId`、`event`、`perPage`、`currentPage` | +| 轨迹详情 | GET | `/mq/trace/detail` | `traceId` | +| Dashboard | GET | `/mq/dashboard` | `window`、`step` | +| 队列深度 | GET | `/mq/queue/depth` | `topic` | +| 清理消息 | POST | `/mq/queue/cleanup` | `status`、`olderThan` | diff --git a/src/components/MessageQueue/TraceList/index.js b/src/components/MessageQueue/TraceList/index.js new file mode 100644 index 0000000..fc291de --- /dev/null +++ b/src/components/MessageQueue/TraceList/index.js @@ -0,0 +1,91 @@ +import { createWithRemoteLoader } from '@kne/remote-loader'; +import { useRef, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import withLocale from '../withLocale'; +import { useIntl } from '@kne/react-intl'; + +import getTraceColumns from '../getTraceColumns'; +import MessageDetail from '../Actions/MessageDetail'; +import Menu from '../Menu'; +import { buildListParams } from '../utils'; + +const TraceList = createWithRemoteLoader({ + modules: ['components-core:Layout@TablePage', 'components-core:Global@usePreset', 'components-core:Filter', 'components-core:Enum'] +})( + withLocale(({ remoteModules, baseUrl, pageProps = {} }) => { + const [TablePage, usePreset, Filter, Enum] = remoteModules; + const { formatMessage } = useIntl(); + const { apis } = usePreset(); + const { SearchInput, getFilterValue, fields: filterFields } = Filter; + const { InputFilterItem, SuperSelectFilterItem } = filterFields; + const [searchParams] = useSearchParams(); + const initialMessageId = searchParams.get('messageId'); + const ref = useRef(null); + const [filter, setFilter] = useState( + initialMessageId + ? [ + { + name: 'messageId', + label: formatMessage({ id: 'MessageId' }), + value: initialMessageId + } + ] + : [] + ); + const filterValue = getFilterValue(filter); + + return ( + { + return { + children: {formatMessage({ id: 'ViewDetail' })} + }; + } + } + ]} + page={{ + filter: { + value: filter, + onChange: setFilter, + list: [ + [ + , + , + { + return ( + + {options => children({ options })} + + ); + }} + /> + ] + ] + }, + titleExtra: , + menu: , + ...pageProps + }} + /> + ); + }) +); + +export default TraceList; diff --git a/src/components/MessageQueue/doc/api.md b/src/components/MessageQueue/doc/api.md new file mode 100644 index 0000000..1401c70 --- /dev/null +++ b/src/components/MessageQueue/doc/api.md @@ -0,0 +1,67 @@ +### MessageQueue + +消息队列管理主组件,内部包含 Dashboard、消息列表、死信列表、轨迹列表和队列工具页。 + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| baseUrl | 组件挂载的基础路由,例如 `/MessageQueue/mq` | string | - | +| pageProps | 传给内部页面组件的布局配置 | object | `{}` | +| children | 自定义顶部导航或附加内容 | ReactNode | - | + +### 子组件 + +| 导出名 | 说明 | +| --- | --- | +| Dashboard | 队列指标概览页 | +| MessageList | 消息列表页,支持发布消息和查看详情 | +| DeadLetterList | 死信列表页,支持单条和批量重放 | +| TraceList | 消息轨迹列表页 | +| QueueTools | 队列深度查询和消息清理工具页 | +| PublishMessage | 发布消息按钮组件 | +| Actions | 消息列表操作按钮 | +| DeadLetterActions | 死信列表操作按钮 | + +### Actions + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| data | 消息记录 | object | - | +| onTrace | 点击查看轨迹时触发 | `(data) => void` | - | +| onSuccess | 操作成功后的回调 | `() => void` | - | +| type | 按钮类型 | string | `default` | + +### DeadLetterActions + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| data | 死信记录 | object | - | +| onSuccess | 重放成功后的回调 | `() => void` | - | +| type | 按钮类型 | string | `default` | + +### PublishMessage + +| 属性名 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| onSuccess | 发布成功后的回调 | `() => void` | - | + +### 枚举 + +| 枚举名 | 值 | +| --- | --- | +| messageStatus | `PENDING`、`PROCESSING`、`COMPLETED`、`FAILED` | +| traceEvent | `PUBLISHED`、`PROCESSING`、`COMPLETED`、`FAILED`、`MOVED_TO_DLQ`、`REPLAYED`、`LOCK_RECOVERED` | +| mqBoolean | `true`、`false` | + +### fastify-mq 接口 + +| 功能 | 方法 | 路径 | 参数 | +| --- | --- | --- | --- | +| 发布消息 | POST | `/mq/message/publish` | `topic`、`payload`、`priority`、`executeAt`、`maxRetries`、`traceId`、`meta` | +| 消息列表 | GET | `/mq/message/list` | `topic`、`status`、`traceId`、`perPage`、`currentPage` | +| 死信列表 | GET | `/mq/dlq/list` | `topic`、`replayed`、`perPage`、`currentPage` | +| 重放死信 | POST | `/mq/dlq/replay` | `id` 或 `ids` | +| 轨迹列表 | GET | `/mq/trace/list` | `topic`、`messageId`、`event`、`perPage`、`currentPage` | +| 轨迹详情 | GET | `/mq/trace/detail` | `traceId` | +| Dashboard | GET | `/mq/dashboard` | `window`、`step` | +| 队列深度 | GET | `/mq/queue/depth` | `topic` | +| 清理消息 | POST | `/mq/queue/cleanup` | `status`、`olderThan` | diff --git a/src/components/MessageQueue/doc/base.js b/src/components/MessageQueue/doc/base.js new file mode 100644 index 0000000..f304754 --- /dev/null +++ b/src/components/MessageQueue/doc/base.js @@ -0,0 +1,37 @@ +const { default: MessageQueue } = _MessageQueue; +const { default: mockPreset } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { useNavigate, Navigate, Route, Routes } = reactRouterDom; +const { Button, Flex } = antd; + +const BaseExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const navigate = useNavigate(); + return ( + + + + + + + + + + + + + } + /> + } /> + + + + ); +}); + +render(); diff --git a/src/components/MessageQueue/doc/dead-letter-replay.js b/src/components/MessageQueue/doc/dead-letter-replay.js new file mode 100644 index 0000000..43f840b --- /dev/null +++ b/src/components/MessageQueue/doc/dead-letter-replay.js @@ -0,0 +1,23 @@ +const { DeadLetterActions } = _MessageQueue; +const { default: mockPreset, deadLetterList } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { Card, Space } = antd; + +const DeadLetterReplayExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const data = deadLetterList.pageData.find(item => !item.replayed); + return ( + + + + {data.topic} + + + + + ); +}); + +render(); diff --git a/src/components/MessageQueue/doc/enums.js b/src/components/MessageQueue/doc/enums.js new file mode 100644 index 0000000..7d5af9b --- /dev/null +++ b/src/components/MessageQueue/doc/enums.js @@ -0,0 +1,33 @@ +const { enums } = _MessageQueue; +const { default: mockPreset } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { Card, Space, Tag } = antd; + +const EnumsExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const getColor = type => ({ success: 'green', danger: 'red', progress: 'blue', info: 'default' })[type] || 'default'; + return ( + + + + + {enums.messageStatus.map(item => ( + {item.value} - {item.description} + ))} + + + + + {enums.traceEvent.map(item => ( + {item.value} - {item.description} + ))} + + + + + ); +}); + +render(); diff --git a/src/components/MessageQueue/doc/example.json b/src/components/MessageQueue/doc/example.json new file mode 100644 index 0000000..64a22ed --- /dev/null +++ b/src/components/MessageQueue/doc/example.json @@ -0,0 +1,50 @@ +{ + "isFull": true, + "list": [ + { + "title": "基础用法", + "description": "完整展示 MessageQueue 的仪表盘、消息列表、死信队列、轨迹追踪和队列工具页。", + "code": "./base.js", + "scope": [ + { "name": "_MessageQueue", "packageName": "@components/MessageQueue" }, + { "name": "_mockPreset", "packageName": "@root/mockPreset" }, + { "name": "remoteLoader", "packageName": "@kne/remote-loader" }, + { "name": "reactRouterDom", "packageName": "react-router-dom" }, + { "name": "antd", "packageName": "antd" } + ] + }, + { + "title": "消息详情", + "description": "展示独立使用消息操作按钮查看消息详情。", + "code": "./message-detail.js", + "scope": [ + { "name": "_MessageQueue", "packageName": "@components/MessageQueue" }, + { "name": "_mockPreset", "packageName": "@root/mockPreset" }, + { "name": "remoteLoader", "packageName": "@kne/remote-loader" }, + { "name": "antd", "packageName": "antd" } + ] + }, + { + "title": "死信重放", + "description": "展示独立使用死信操作按钮提交重放。", + "code": "./dead-letter-replay.js", + "scope": [ + { "name": "_MessageQueue", "packageName": "@components/MessageQueue" }, + { "name": "_mockPreset", "packageName": "@root/mockPreset" }, + { "name": "remoteLoader", "packageName": "@kne/remote-loader" }, + { "name": "antd", "packageName": "antd" } + ] + }, + { + "title": "枚举值", + "description": "展示消息状态和轨迹事件枚举。", + "code": "./enums.js", + "scope": [ + { "name": "_MessageQueue", "packageName": "@components/MessageQueue" }, + { "name": "_mockPreset", "packageName": "@root/mockPreset" }, + { "name": "remoteLoader", "packageName": "@kne/remote-loader" }, + { "name": "antd", "packageName": "antd" } + ] + } + ] +} diff --git a/src/components/MessageQueue/doc/message-detail.js b/src/components/MessageQueue/doc/message-detail.js new file mode 100644 index 0000000..ad80a07 --- /dev/null +++ b/src/components/MessageQueue/doc/message-detail.js @@ -0,0 +1,23 @@ +const { Actions } = _MessageQueue; +const { default: mockPreset, messageQueueList } = _mockPreset; +const { createWithRemoteLoader } = remoteLoader; +const { Card, Space } = antd; + +const MessageDetailExample = createWithRemoteLoader({ + modules: ['components-core:Global@PureGlobal', 'components-core:Layout'] +})(({ remoteModules }) => { + const [PureGlobal, Layout] = remoteModules; + const data = messageQueueList.pageData[0]; + return ( + + + + {data.topic} + + + + + ); +}); + +render(); diff --git a/src/components/MessageQueue/doc/style.scss b/src/components/MessageQueue/doc/style.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/MessageQueue/doc/summary.md b/src/components/MessageQueue/doc/summary.md new file mode 100644 index 0000000..50f92dd --- /dev/null +++ b/src/components/MessageQueue/doc/summary.md @@ -0,0 +1,5 @@ +MessageQueue 是面向 `fastify-mq` 的消息队列管理端组件,提供队列运行概览、消息发布与查询、死信处理、轨迹追踪和队列维护工具。 + +组件直接对齐 `fastify-mq` 的接口契约,列表筛选使用后端支持的扁平查询参数,死信支持单条与批量重放,Dashboard 展示队列深度、消费速率、失败率、死信速率和成功率等关键指标。 + +适用于需要在后台管理系统中观察异步消息处理状态、排查失败消息、追踪消息生命周期,以及执行基础队列运维操作的业务场景。 diff --git a/src/components/MessageQueue/enums.js b/src/components/MessageQueue/enums.js new file mode 100644 index 0000000..124f3f1 --- /dev/null +++ b/src/components/MessageQueue/enums.js @@ -0,0 +1,29 @@ +const MESSAGE_STATUS_ENUM = [ + { value: 'PENDING', description: '等待执行', type: 'info' }, + { value: 'PROCESSING', description: '处理中', type: 'progress' }, + { value: 'COMPLETED', description: '已完成', type: 'success' }, + { value: 'FAILED', description: '失败', type: 'danger' } +]; + +const TRACE_EVENT_ENUM = [ + { value: 'PUBLISHED', description: '消息发布', type: 'info' }, + { value: 'PROCESSING', description: '开始处理', type: 'progress' }, + { value: 'COMPLETED', description: '处理完成', type: 'success' }, + { value: 'FAILED', description: '处理失败', type: 'danger' }, + { value: 'MOVED_TO_DLQ', description: '进入死信', type: 'danger' }, + { value: 'REPLAYED', description: '死信重放', type: 'success' }, + { value: 'LOCK_RECOVERED', description: '锁定恢复', type: 'info' } +]; + +const YES_NO_ENUM = [ + { value: true, description: '是', type: 'success' }, + { value: false, description: '否', type: 'info' } +]; + +const enums = { + messageStatus: MESSAGE_STATUS_ENUM, + traceEvent: TRACE_EVENT_ENUM, + mqBoolean: YES_NO_ENUM +}; + +export default enums; diff --git a/src/components/MessageQueue/getColumns.js b/src/components/MessageQueue/getColumns.js new file mode 100644 index 0000000..400a418 --- /dev/null +++ b/src/components/MessageQueue/getColumns.js @@ -0,0 +1,99 @@ +import withLocale from './withLocale'; +import { useIntl } from '@kne/react-intl'; +import { stringifyJson } from './utils'; + +const getColumns = ({ formatMessage }) => { + return [ + { + name: 'id', + title: formatMessage({ id: 'ID' }), + type: 'serialNumber' + }, + { + name: 'topic', + title: formatMessage({ id: 'Topic' }), + type: 'tag', + ellipsis: true, + valueOf: ({ topic }) => + topic && { + type: 'info', + text: topic + } + }, + { + name: 'status', + title: formatMessage({ id: 'Status' }), + type: 'tag', + valueOf: ({ status }) => + status && { + isEnum: true, + moduleName: 'messageStatus', + name: status + } + }, + { + name: 'payload', + title: formatMessage({ id: 'Payload' }), + ellipsis: true, + valueOf: ({ payload }) => stringifyJson(payload) + }, + { + name: 'retryCount', + title: formatMessage({ id: 'RetryCount' }), + width: 100 + }, + { + name: 'maxRetries', + title: formatMessage({ id: 'MaxRetries' }), + width: 100 + }, + { + name: 'priority', + title: formatMessage({ id: 'Priority' }), + width: 80 + }, + { + name: 'traceId', + title: formatMessage({ id: 'TraceId' }), + ellipsis: true, + copyable: true + }, + { + name: 'consumerId', + title: formatMessage({ id: 'ConsumerId' }), + ellipsis: true + }, + { + name: 'executeAt', + title: formatMessage({ id: 'ExecuteAt' }), + type: 'datetime' + }, + { + name: 'nextRetryAt', + title: formatMessage({ id: 'NextRetryAt' }), + type: 'datetime' + }, + { + name: 'lockedAt', + title: formatMessage({ id: 'LockedAt' }), + type: 'datetime' + }, + { + name: 'createdAt', + title: formatMessage({ id: 'CreatedAt' }), + type: 'datetime' + }, + { + name: 'updatedAt', + title: formatMessage({ id: 'UpdatedAt' }), + type: 'datetime' + } + ]; +}; + +export const ColumnsLoader = withLocale(({ children }) => { + const { formatMessage } = useIntl(); + return children(props => getColumns(Object.assign({}, props, { formatMessage }))); +}); + +export default getColumns; diff --git a/src/components/MessageQueue/getDeadLetterColumns.js b/src/components/MessageQueue/getDeadLetterColumns.js new file mode 100644 index 0000000..bee61de --- /dev/null +++ b/src/components/MessageQueue/getDeadLetterColumns.js @@ -0,0 +1,69 @@ +import withLocale from './withLocale'; +import { useIntl } from '@kne/react-intl'; +import { stringifyJson } from './utils'; + +const getDeadLetterColumns = ({ formatMessage }) => { + return [ + { + name: 'id', + title: formatMessage({ id: 'ID' }), + type: 'serialNumber' + }, + { + name: 'topic', + title: formatMessage({ id: 'Topic' }), + type: 'tag', + ellipsis: true, + valueOf: ({ topic }) => + topic && { + type: 'info', + text: topic + } + }, + { + name: 'originalId', + title: formatMessage({ id: 'OriginalMessageId' }), + ellipsis: true, + copyable: true + }, + { + name: 'errorMessage', + title: formatMessage({ id: 'ErrorMessage' }), + ellipsis: true, + width: 200 + }, + { + name: 'payload', + title: formatMessage({ id: 'Payload' }), + ellipsis: true, + valueOf: ({ payload }) => stringifyJson(payload) + }, + { + name: 'replayed', + title: formatMessage({ id: 'Replayed' }), + type: 'tag', + valueOf: ({ replayed }) => ({ + isEnum: true, + moduleName: 'mqBoolean', + name: !!replayed + }) + }, + { + name: 'replayedAt', + title: formatMessage({ id: 'ReplayedAt' }), + type: 'datetime' + }, + { + name: 'createdAt', + title: formatMessage({ id: 'CreatedAt' }), + type: 'datetime' + } + ]; +}; + +export const DeadLetterColumnsLoader = withLocale(({ children }) => { + const { formatMessage } = useIntl(); + return children(props => getDeadLetterColumns(Object.assign({}, props, { formatMessage }))); +}); + +export default getDeadLetterColumns; diff --git a/src/components/MessageQueue/getTraceColumns.js b/src/components/MessageQueue/getTraceColumns.js new file mode 100644 index 0000000..5b25e37 --- /dev/null +++ b/src/components/MessageQueue/getTraceColumns.js @@ -0,0 +1,64 @@ +import withLocale from './withLocale'; +import { useIntl } from '@kne/react-intl'; +import { stringifyJson } from './utils'; + +const getTraceColumns = ({ formatMessage }) => { + return [ + { + name: 'id', + title: formatMessage({ id: 'ID' }), + type: 'serialNumber' + }, + { + name: 'traceId', + title: formatMessage({ id: 'TraceId' }), + ellipsis: true, + copyable: true + }, + { + name: 'topic', + title: formatMessage({ id: 'Topic' }), + type: 'tag', + valueOf: ({ topic }) => + topic && { + type: 'info', + text: topic + } + }, + { + name: 'event', + title: formatMessage({ id: 'Event' }), + type: 'tag', + valueOf: ({ event }) => ({ + isEnum: true, + moduleName: 'traceEvent', + name: event + }) + }, + { + name: 'messageId', + title: formatMessage({ id: 'MessageId' }), + ellipsis: true, + copyable: true + }, + { + name: 'detail', + title: formatMessage({ id: 'Detail' }), + ellipsis: true, + width: 200, + valueOf: ({ detail }) => stringifyJson(detail) + }, + { + name: 'createdAt', + title: formatMessage({ id: 'CreatedAt' }), + type: 'datetime' + } + ]; +}; + +export const TraceColumnsLoader = withLocale(({ children }) => { + const { formatMessage } = useIntl(); + return children(props => getTraceColumns(Object.assign({}, props, { formatMessage }))); +}); + +export default getTraceColumns; diff --git a/src/components/MessageQueue/index.js b/src/components/MessageQueue/index.js new file mode 100644 index 0000000..565d9fb --- /dev/null +++ b/src/components/MessageQueue/index.js @@ -0,0 +1,49 @@ +import AppChildrenRouter from '@kne/app-children-router'; +import Dashboard from './Dashboard'; +import MessageList from './MessageList'; +import DeadLetterList from './DeadLetterList'; +import TraceList from './TraceList'; +import QueueTools from './QueueTools'; + +const MessageQueue = ({ baseUrl, children, ...props }) => { + return ( + + }, + { + path: 'messages', + element: + }, + { + path: 'dead-letter', + element: + }, + { + path: 'traces', + element: + }, + { + path: 'tools', + element: + } + ]}> + {children} + + ); +}; + +export default MessageQueue; + +export { default as enums } from './enums'; +export { default as getColumns, ColumnsLoader } from './getColumns'; +export { default as getDeadLetterColumns, DeadLetterColumnsLoader } from './getDeadLetterColumns'; +export { default as getTraceColumns, TraceColumnsLoader } from './getTraceColumns'; +export { default as Actions } from './Actions'; +export { default as DeadLetterActions } from './Actions/DeadLetterActions'; +export { default as Dashboard } from './Dashboard'; +export { default as PublishMessage } from './PublishMessage'; +export { default as QueueTools } from './QueueTools'; +export { MessageList, DeadLetterList, TraceList }; diff --git a/src/components/MessageQueue/locale/en-US.js b/src/components/MessageQueue/locale/en-US.js new file mode 100644 index 0000000..06e035f --- /dev/null +++ b/src/components/MessageQueue/locale/en-US.js @@ -0,0 +1,80 @@ +const messages = { + ID: 'ID', + Topic: 'Topic', + Status: 'Status', + Payload: 'Payload', + Options: 'Options', + ConsumerId: 'Consumer', + LockedAt: 'Locked At', + NextRetryAt: 'Next Retry At', + RetryCount: 'Retry Count', + MaxRetries: 'Max Retries', + Priority: 'Priority', + TraceId: 'Trace ID', + MessageId: 'Message ID', + ExecuteAt: 'Execute At', + CreatedAt: 'Created At', + UpdatedAt: 'Updated At', + CompletedAt: 'Completed At', + Operation: 'Operation', + ViewDetail: 'View Detail', + ViewTrace: 'View Trace', + MessageDetail: 'Message Detail', + MessageList: 'Message List', + DeadLetterList: 'Dead Letter List', + TraceList: 'Trace List', + Dashboard: 'Dashboard', + Tools: 'Tools', + PublishMessage: 'Publish Message', + PublishSuccess: 'Message published', + QueueTools: 'Queue Tools', + Refresh: 'Refresh', + RealtimeConnected: 'Realtime connected', + RealtimeDisconnected: 'Realtime disconnected', + LastUpdatedAt: 'Last Updated At', + QueryDepth: 'Query Depth', + CleanupMessages: 'Cleanup Messages', + CleanupSuccess: 'Messages cleaned up', + CleanupConfirm: 'Are you sure you want to clean up matching messages?', + DepthResult: 'Current Queue Depth', + QueueDepth: 'Queue Depth', + ConsumedTotal: 'Consumed Total', + FailedTotal: 'Failed Total', + DLQTotal: 'Dead Letter Total', + ConsumeRate: 'Consume Rate', + FailureRate: 'Failure Rate', + DLQRate: 'DLQ Rate', + SuccessRatio: 'Success Ratio', + Replay: 'Replay', + ReplayMessage: 'Replay Message', + ReplayMessageConfirm: 'Are you sure you want to replay this message?', + Replayed: 'Replayed', + ErrorMessage: 'Error Message', + OriginalMessageId: 'Original Message ID', + ReplayedAt: 'Replayed At', + BatchReplay: 'Batch Replay', + BatchReplayConfirm: 'Replay selected dead-letter messages?', + ReplaySuccess: 'Dead-letter replay submitted', + SelectedCount: '{count} selected', + Event: 'Event', + Detail: 'Detail', + Meta: 'Meta', + Window: 'Window', + Step: 'Step', + OlderThan: 'Older Than', + JsonInvalid: '{name} must be valid JSON', + InputJsonPlaceholder: 'Input a JSON object', + TopicPlaceholder: 'Input topic', + PENDING: 'Pending', + PROCESSING: 'Processing', + COMPLETED: 'Completed', + FAILED: 'Failed', + PUBLISHED: 'Published', + MOVED_TO_DLQ: 'Moved to DLQ', + REPLAYED: 'Replayed', + LOCK_RECOVERED: 'Lock Recovered', + Yes: 'Yes', + No: 'No' +}; + +export default messages; diff --git a/src/components/MessageQueue/locale/zh-CN.js b/src/components/MessageQueue/locale/zh-CN.js new file mode 100644 index 0000000..e756a8f --- /dev/null +++ b/src/components/MessageQueue/locale/zh-CN.js @@ -0,0 +1,80 @@ +const messages = { + ID: '编号', + Topic: '主题', + Status: '状态', + Payload: '消息内容', + Options: '扩展字段', + ConsumerId: '消费者', + LockedAt: '锁定时间', + NextRetryAt: '下次重试', + RetryCount: '重试次数', + MaxRetries: '最大重试', + Priority: '优先级', + TraceId: '追踪ID', + MessageId: '消息ID', + ExecuteAt: '执行时间', + CreatedAt: '创建时间', + UpdatedAt: '更新时间', + CompletedAt: '完成时间', + Operation: '操作', + ViewDetail: '查看详情', + ViewTrace: '查看轨迹', + MessageDetail: '消息详情', + MessageList: '消息列表', + DeadLetterList: '死信列表', + TraceList: '轨迹列表', + Dashboard: '仪表盘', + Tools: '工具', + PublishMessage: '发布消息', + PublishSuccess: '消息发布成功', + QueueTools: '队列工具', + Refresh: '刷新', + RealtimeConnected: '实时连接中', + RealtimeDisconnected: '实时连接已断开', + LastUpdatedAt: '最后更新', + QueryDepth: '查询深度', + CleanupMessages: '清理消息', + CleanupSuccess: '消息清理成功', + CleanupConfirm: '确定要清理符合条件的消息吗?', + DepthResult: '当前队列深度', + QueueDepth: '队列深度', + ConsumedTotal: '累计消费', + FailedTotal: '累计失败', + DLQTotal: '累计死信', + ConsumeRate: '消费速率', + FailureRate: '失败速率', + DLQRate: '死信速率', + SuccessRatio: '成功率', + Replay: '重放', + ReplayMessage: '重放消息', + ReplayMessageConfirm: '确定要重放这条消息吗?', + Replayed: '已重放', + ErrorMessage: '错误信息', + OriginalMessageId: '原始消息ID', + ReplayedAt: '重放时间', + BatchReplay: '批量重放', + BatchReplayConfirm: '确定要重放选中的死信消息吗?', + ReplaySuccess: '死信已提交重放', + SelectedCount: '已选择 {count} 条', + Event: '事件', + Detail: '详情', + Meta: '元数据', + Window: '统计窗口', + Step: '采样步长', + OlderThan: '早于时间', + JsonInvalid: '{name} 必须是合法 JSON', + InputJsonPlaceholder: '请输入 JSON 对象', + TopicPlaceholder: '请输入消息主题', + PENDING: '等待执行', + PROCESSING: '处理中', + COMPLETED: '已完成', + FAILED: '失败', + PUBLISHED: '消息发布', + MOVED_TO_DLQ: '进入死信', + REPLAYED: '死信重放', + LOCK_RECOVERED: '锁定恢复', + Yes: '是', + No: '否' +}; + +export default messages; diff --git a/src/components/MessageQueue/style.module.scss b/src/components/MessageQueue/style.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/components/MessageQueue/utils.js b/src/components/MessageQueue/utils.js new file mode 100644 index 0000000..dd8976d --- /dev/null +++ b/src/components/MessageQueue/utils.js @@ -0,0 +1,151 @@ +const isPlainObject = value => Object.prototype.toString.call(value) === '[object Object]'; + +export const unwrapFilterValue = value => { + if (value === undefined || value === null || value === '') { + return undefined; + } + + if (Array.isArray(value)) { + const list = value.map(item => unwrapFilterValue(item)).filter(item => item !== undefined); + return list.length > 0 ? list : undefined; + } + + if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, 'value')) { + return unwrapFilterValue(value.value); + } + + return value; +}; + +export const normalizeFilterValue = filterValue => { + return Object.keys(filterValue || {}).reduce((result, key) => { + const value = unwrapFilterValue(filterValue[key]); + if (value !== undefined) { + result[key] = value; + } + return result; + }, {}); +}; + +export const buildListParams = (filterValue, allowedKeys) => { + const normalized = normalizeFilterValue(filterValue); + return (allowedKeys || Object.keys(normalized)).reduce((result, key) => { + if (normalized[key] !== undefined) { + result[key] = normalized[key]; + } + return result; + }, {}); +}; + +export const parseJsonInput = (value, fieldName, defaultValue = {}) => { + if (value === undefined || value === null || value === '') { + return defaultValue; + } + + if (typeof value === 'object') { + return value; + } + + try { + return JSON.parse(value); + } catch (error) { + throw new Error(`${fieldName} must be valid JSON`); + } +}; + +export const parseNumberInput = (value, fieldName, defaultValue) => { + if (value === undefined || value === null || value === '') { + return defaultValue; + } + + const numberValue = Number(value); + if (Number.isNaN(numberValue)) { + throw new Error(`${fieldName} must be a valid number`); + } + + return numberValue; +}; + +export const parseIsoDateTimeInput = (value, fieldName) => { + if (value === undefined || value === null || value === '') { + return undefined; + } + + const dateTimePattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/; + if (!dateTimePattern.test(value) || Number.isNaN(Date.parse(value))) { + throw new Error(`${fieldName} must be a valid ISO date-time`); + } + + return value; +}; + +export const stringifyJson = value => { + if (value === undefined || value === null || value === '') { + return ''; + } + + if (typeof value === 'string') { + return value; + } + + return JSON.stringify(value, null, 2); +}; + +export const formatRate = value => { + const numberValue = Number(value || 0); + return numberValue.toFixed(2); +}; + +export const formatPercent = value => { + if (value === undefined || value === null || Number.isNaN(Number(value))) { + return '-'; + } + + return `${(Number(value) * 100).toFixed(2)}%`; +}; + +export const buildUrlWithParams = (url, params = {}) => { + const query = Object.keys(params) + .filter(key => params[key] !== undefined && params[key] !== null && params[key] !== '') + .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) + .join('&'); + + if (!query) { + return url; + } + + return `${url}${url.includes('?') ? '&' : '?'}${query}`; +}; + +export const getMetricTotal = value => { + if (!value) { + return 0; + } + + if (typeof value.total === 'number') { + return value.total; + } + + return Object.values(value.byTopic || {}).reduce((total, item) => total + Number(item || 0), 0); +}; + +export const filterPageData = ({ pageData = [], params = {}, filters = {} }) => { + const normalizedParams = Object.assign({}, params, params.filter); + const filtered = pageData.filter(item => { + return Object.keys(filters).every(key => { + const value = unwrapFilterValue(normalizedParams[key]); + if (value === undefined) { + return true; + } + return filters[key](item, value); + }); + }); + const currentPage = Number(normalizedParams.currentPage || 1); + const perPage = Number(normalizedParams.perPage || filtered.length || 20); + const start = perPage * (currentPage - 1); + + return { + pageData: filtered.slice(start, start + perPage), + totalCount: filtered.length + }; +}; diff --git a/src/components/MessageQueue/utils.test.js b/src/components/MessageQueue/utils.test.js new file mode 100644 index 0000000..7404fef --- /dev/null +++ b/src/components/MessageQueue/utils.test.js @@ -0,0 +1,88 @@ +import { + buildListParams, + buildUrlWithParams, + filterPageData, + formatPercent, + formatRate, + parseIsoDateTimeInput, + parseJsonInput, + parseNumberInput +} from './utils'; + +describe('MessageQueue utils', () => { + test('builds flat query params from Filter values for fastify-mq list APIs', () => { + const params = buildListParams( + { + topic: { value: 'order.created', label: 'order.created' }, + status: { value: { value: 'FAILED', label: 'Failed' } }, + traceId: 'trace_001', + empty: '', + ignored: 'ignored' + }, + ['topic', 'status', 'traceId'] + ); + + expect(params).toEqual({ + topic: 'order.created', + status: 'FAILED', + traceId: 'trace_001' + }); + }); + + test('keeps boolean values when building dead-letter filters', () => { + const params = buildListParams( + { + replayed: { value: false, label: 'No' }, + topic: { value: 'email.send' } + }, + ['topic', 'replayed'] + ); + + expect(params).toEqual({ + topic: 'email.send', + replayed: false + }); + }); + + test('parses JSON form input and reports invalid JSON', () => { + expect(parseJsonInput('{"orderId":"ord_001"}', 'payload')).toEqual({ orderId: 'ord_001' }); + expect(parseJsonInput({ orderId: 'ord_002' }, 'payload')).toEqual({ orderId: 'ord_002' }); + expect(parseJsonInput('', 'meta')).toEqual({}); + expect(() => parseJsonInput('{bad json}', 'payload')).toThrow('payload'); + }); + + test('parses numeric and ISO date-time form input before publishing', () => { + expect(parseNumberInput('3', 'maxRetries')).toBe(3); + expect(parseNumberInput('', 'priority', 0)).toBe(0); + expect(() => parseNumberInput('abc', 'priority')).toThrow('priority'); + expect(parseIsoDateTimeInput('2026-05-11T09:00:00.000Z', 'executeAt')).toBe('2026-05-11T09:00:00.000Z'); + expect(parseIsoDateTimeInput('', 'executeAt')).toBeUndefined(); + expect(() => parseIsoDateTimeInput('2026-05-11', 'executeAt')).toThrow('executeAt'); + }); + + test('filters and paginates mock page data', () => { + const result = filterPageData({ + pageData: [{ topic: 'a' }, { topic: 'a' }, { topic: 'a' }, { topic: 'b' }], + params: { topic: 'a', currentPage: 2, perPage: 2 }, + filters: { + topic: (item, value) => item.topic === value + } + }); + + expect(result).toEqual({ + pageData: [{ topic: 'a' }], + totalCount: 3 + }); + }); + + test('formats rate and percent values for dashboard display', () => { + expect(formatRate(1.23456)).toBe('1.23'); + expect(formatPercent(0.9632)).toBe('96.32%'); + expect(formatPercent(null)).toBe('-'); + }); + + test('builds dashboard SSE url with query params', () => { + expect(buildUrlWithParams('/api/v1/mq/dashboard/sse', { interval: 1000, window: 300000 })).toBe('/api/v1/mq/dashboard/sse?interval=1000&window=300000'); + expect(buildUrlWithParams('/api/v1/mq/dashboard/sse?foo=bar', { interval: 1000 })).toBe('/api/v1/mq/dashboard/sse?foo=bar&interval=1000'); + }); +}); diff --git a/src/components/MessageQueue/withLocale.js b/src/components/MessageQueue/withLocale.js new file mode 100644 index 0000000..64d5e41 --- /dev/null +++ b/src/components/MessageQueue/withLocale.js @@ -0,0 +1,14 @@ +import { createWithIntlProvider } from '@kne/react-intl'; +import zhCN from './locale/zh-CN'; +import enUS from './locale/en-US'; + +const withLocale = createWithIntlProvider({ + defaultLocale: 'zh-CN', + messages: { + 'zh-CN': zhCN, + 'en-US': enUS + }, + namespace: 'components-admin:MessageQueue' +}); + +export default withLocale; diff --git a/src/mockPreset/dead-letter-list.json b/src/mockPreset/dead-letter-list.json new file mode 100644 index 0000000..e8258b9 --- /dev/null +++ b/src/mockPreset/dead-letter-list.json @@ -0,0 +1,60 @@ +{ + "pageData": [ + { + "id": "dlq_001", + "originalId": "msg_004", + "topic": "email.send", + "payload": { "to": "user@example.com", "template": "welcome", "vars": { "name": "John" } }, + "errorMessage": "SMTP connection timeout after 30000ms", + "replayed": false, + "replayedAt": null, + "createdAt": "2026-05-10T09:50:00Z", + "updatedAt": "2026-05-10T09:50:00Z" + }, + { + "id": "dlq_002", + "originalId": "msg_008", + "topic": "file.process", + "payload": { "fileId": "file_555", "type": "image", "operations": ["resize", "compress"] }, + "errorMessage": "File too large: exceeds 10MB limit", + "replayed": false, + "replayedAt": null, + "createdAt": "2026-05-10T09:40:00Z", + "updatedAt": "2026-05-10T09:40:00Z" + }, + { + "id": "dlq_003", + "originalId": "msg_010", + "topic": "payment.process", + "payload": { "paymentId": "pay_456", "amount": 2500.00 }, + "errorMessage": "Payment gateway error: invalid merchant ID", + "replayed": true, + "replayedAt": "2026-05-10T08:30:00Z", + "createdAt": "2026-05-10T08:00:00Z", + "updatedAt": "2026-05-10T08:30:00Z" + }, + { + "id": "dlq_004", + "originalId": "msg_012", + "topic": "sms.send", + "payload": { "phone": "+1234567890", "message": "Your code is 123456" }, + "errorMessage": "Invalid phone number format", + "replayed": false, + "replayedAt": null, + "createdAt": "2026-05-09T15:20:00Z", + "updatedAt": "2026-05-09T15:20:00Z" + }, + { + "id": "dlq_005", + "originalId": "msg_015", + "topic": "webhook.invoke", + "payload": { "url": "https://example.com/webhook", "event": "order.created" }, + "errorMessage": "Webhook endpoint returned 503 Service Unavailable", + "replayed": true, + "replayedAt": "2026-05-09T12:00:00Z", + "createdAt": "2026-05-09T11:30:00Z", + "updatedAt": "2026-05-09T12:00:00Z" + } + ], + "totalCount": 5 +} diff --git a/src/mockPreset/index.js b/src/mockPreset/index.js index 740e0d3..6c71608 100644 --- a/src/mockPreset/index.js +++ b/src/mockPreset/index.js @@ -3,6 +3,7 @@ import { getApis } from '@components/Apis'; import { enums as taskEnums } from '@components/Task'; import { enums as intlAdminEnums } from '@components/IntlAdmin'; import merge from 'lodash/merge'; +import { filterPageData } from '@components/MessageQueue/utils'; import taskList from './task-list.json'; import signatureList from './signature-list.json'; @@ -13,8 +14,11 @@ import superAdminInfo from './super-admin-info.json'; import groupList from './group-list.json'; import tenantData from './tenant-data.json'; import tenantAdminData from './tenant-admin-data.json'; +import messageQueueList from './message-queue-list.json'; +import deadLetterList from './dead-letter-list.json'; +import traceList from './trace-list.json'; -export { taskList, signatureList, intlAdminData, adminUserList, userInfo, superAdminInfo, groupList, tenantData, tenantAdminData }; +export { taskList, signatureList, intlAdminData, adminUserList, userInfo, superAdminInfo, groupList, tenantData, tenantAdminData, messageQueueList, deadLetterList, traceList }; const apis = merge({}, getApis(), { task: { @@ -433,6 +437,141 @@ const apis = merge({}, getApis(), { removeCustomComponent: { loader: () => ({ code: 0 }) } + }, + mq: { + message: { + publish: { + loader: ({ data }) => ({ + id: `msg_${Date.now()}`, + traceId: data?.traceId || `trace_${Date.now()}`, + status: 'PENDING', + ...data + }) + }, + list: { + loader: ({ params }) => { + return filterPageData({ + pageData: messageQueueList.pageData, + params, + filters: { + topic: (item, value) => item.topic === value, + status: (item, value) => item.status === value, + traceId: (item, value) => item.traceId === value + } + }); + } + } + }, + deadLetter: { + list: { + loader: ({ params }) => { + return filterPageData({ + pageData: deadLetterList.pageData, + params, + filters: { + topic: (item, value) => item.topic === value, + replayed: (item, value) => item.replayed === value + } + }); + } + }, + replay: { + loader: ({ data }) => { + if (Array.isArray(data?.ids) && data.ids.length > 0) { + return data.ids.map(id => ({ id, success: true, messageId: `msg_replay_${Date.now()}` })); + } + return { + id: `msg_replay_${Date.now()}`, + originalDeadLetterId: data?.id, + status: 'PENDING' + }; + } + } + }, + trace: { + list: { + loader: ({ params }) => { + return filterPageData({ + pageData: traceList.pageData, + params, + filters: { + topic: (item, value) => item.topic === value, + messageId: (item, value) => item.messageId === value, + event: (item, value) => item.event === value + } + }); + } + }, + detail: { + loader: ({ params }) => { + const traceId = params?.traceId || 'trace_abc123'; + return traceList.pageData.filter(item => item.traceId === traceId); + } + } + }, + dashboard: { + getData: { + loader: () => ({ + timestamp: Date.now(), + current: { + queueDepth: { byTopic: { 'order.created': 5, 'user.registered': 2, 'payment.completed': 1 }, total: 8 }, + consumedTotal: { byTopic: { 'order.created': 100, 'user.registered': 50 }, total: 150 }, + failedTotal: { byTopic: { 'email.send': 2, 'file.process': 1 }, total: 3 }, + dlqTotal: { byTopic: { 'email.send': 2, 'file.process': 1 }, total: 3 }, + consumeRate: { byTopic: { 'order.created': 0.5, 'user.registered': 0.3 }, total: 0.8 }, + failureRate: { byTopic: { 'email.send': 0.02 }, total: 0.02 }, + dlqRate: { byTopic: { 'email.send': 0.01 }, total: 0.01 }, + successRatio: 0.96, + successRatioByTopic: { 'order.created': 0.96, 'user.registered': 0.98 } + }, + timeSeries: { + queueDepth: [ + { timestamp: Date.now() - 300000, 'order.created': 8, 'user.registered': 3 }, + { timestamp: Date.now() - 240000, 'order.created': 7, 'user.registered': 2 }, + { timestamp: Date.now() - 180000, 'order.created': 6, 'user.registered': 2 }, + { timestamp: Date.now() - 120000, 'order.created': 5, 'user.registered': 2 }, + { timestamp: Date.now() - 60000, 'order.created': 5, 'user.registered': 1 }, + { timestamp: Date.now(), 'order.created': 5, 'user.registered': 2 } + ], + consumeRate: [ + { timestamp: Date.now() - 300000, 'order.created': 0.5, 'user.registered': 0.3 }, + { timestamp: Date.now() - 240000, 'order.created': 0.6, 'user.registered': 0.2 }, + { timestamp: Date.now() - 180000, 'order.created': 0.4, 'user.registered': 0.4 }, + { timestamp: Date.now() - 120000, 'order.created': 0.5, 'user.registered': 0.3 }, + { timestamp: Date.now() - 60000, 'order.created': 0.5, 'user.registered': 0.3 }, + { timestamp: Date.now(), 'order.created': 0.5, 'user.registered': 0.3 } + ], + failureRate: [ + { timestamp: Date.now() - 300000, 'email.send': 0.02 }, + { timestamp: Date.now() - 240000, 'email.send': 0.01 }, + { timestamp: Date.now() - 180000, 'email.send': 0.03 }, + { timestamp: Date.now() - 120000, 'email.send': 0.02 }, + { timestamp: Date.now() - 60000, 'email.send': 0.01 }, + { timestamp: Date.now(), 'email.send': 0.02 } + ], + dlqRate: [ + { timestamp: Date.now() - 300000, 'email.send': 0.01 }, + { timestamp: Date.now() - 240000, 'email.send': 0.005 }, + { timestamp: Date.now() - 180000, 'email.send': 0.015 }, + { timestamp: Date.now() - 120000, 'email.send': 0.01 }, + { timestamp: Date.now() - 60000, 'email.send': 0.005 }, + { timestamp: Date.now(), 'email.send': 0.01 } + ] + } + }) + } + }, + queue: { + depth: { + loader: ({ params }) => { + const list = messageQueueList.pageData.filter(item => item.status === 'PENDING' && (!params?.topic || item.topic === params.topic)); + return { depth: list.length }; + } + }, + cleanup: { + loader: () => ({ deleted: 3 }) + } + } } }); @@ -444,6 +583,29 @@ const enums = Object.assign({}, taskEnums, intlAdminEnums, { { value: 'video_processing', description: '视频处理' }, { value: 'data_sync', description: '数据同步' }, { value: 'report_generation', description: '报表生成' } + ], + messageStatus: [ + { value: 'PENDING', description: '等待执行', type: 'info' }, + { value: 'PROCESSING', description: '处理中', type: 'progress' }, + { value: 'COMPLETED', description: '已完成', type: 'success' }, + { value: 'FAILED', description: '失败', type: 'danger' } + ], + traceEvent: [ + { value: 'PUBLISHED', description: '消息发布', type: 'info' }, + { value: 'PROCESSING', description: '开始处理', type: 'progress' }, + { value: 'COMPLETED', description: '处理完成', type: 'success' }, + { value: 'FAILED', description: '处理失败', type: 'danger' }, + { value: 'MOVED_TO_DLQ', description: '进入死信', type: 'danger' }, + { value: 'REPLAYED', description: '死信重放', type: 'success' }, + { value: 'LOCK_RECOVERED', description: '锁定恢复', type: 'info' } + ], + mqBoolean: [ + { value: true, description: '是', type: 'success' }, + { value: false, description: '否', type: 'info' } + ], + yesNo: [ + { value: 'yes', description: '是' }, + { value: 'no', description: '否' } ] }); @@ -453,7 +615,7 @@ const preset = { const { ajax } = await globalInit(); return ajax({ loader, ...props }); } - return Promise.resolve({ data: loader ? { code: 0, data: loader() } : { code: 0, data: {} } }); + return Promise.resolve({ data: loader ? { code: 0, data: loader(props) } : { code: 0, data: {} } }); }, apis, enums, diff --git a/src/mockPreset/message-queue-list.json b/src/mockPreset/message-queue-list.json new file mode 100644 index 0000000..077a2a2 --- /dev/null +++ b/src/mockPreset/message-queue-list.json @@ -0,0 +1,141 @@ +{ + "pageData": [ + { + "id": "msg_001", + "topic": "order.created", + "payload": { "orderId": "ord_12345", "amount": 299.99, "customerId": "cust_001" }, + "status": "COMPLETED", + "retryCount": 0, + "maxRetries": 3, + "priority": 1, + "executeAt": "2026-05-10T10:00:00Z", + "nextRetryAt": null, + "consumerId": "consumer_01", + "lockedAt": "2026-05-10T10:00:01Z", + "traceId": "trace_abc123", + "options": {}, + "createdAt": "2026-05-10T10:00:00Z", + "updatedAt": "2026-05-10T10:00:05Z" + }, + { + "id": "msg_002", + "topic": "user.registered", + "payload": { "userId": "user_54321", "email": "test@example.com" }, + "status": "PROCESSING", + "retryCount": 1, + "maxRetries": 3, + "priority": 2, + "executeAt": "2026-05-10T10:05:00Z", + "nextRetryAt": "2026-05-10T10:06:00Z", + "consumerId": "consumer_02", + "lockedAt": "2026-05-10T10:05:01Z", + "traceId": "trace_def456", + "options": { "timeout": 30000 }, + "createdAt": "2026-05-10T10:05:00Z", + "updatedAt": "2026-05-10T10:05:30Z" + }, + { + "id": "msg_003", + "topic": "payment.completed", + "payload": { "paymentId": "pay_789", "amount": 1500.00, "method": "credit_card" }, + "status": "PENDING", + "retryCount": 0, + "maxRetries": 3, + "priority": 3, + "executeAt": "2026-05-10T10:10:00Z", + "nextRetryAt": null, + "consumerId": null, + "lockedAt": null, + "traceId": "trace_ghi789", + "options": {}, + "createdAt": "2026-05-10T10:10:00Z", + "updatedAt": "2026-05-10T10:10:00Z" + }, + { + "id": "msg_004", + "topic": "email.send", + "payload": { "to": "user@example.com", "template": "welcome", "vars": { "name": "John" } }, + "status": "FAILED", + "retryCount": 3, + "maxRetries": 3, + "priority": 0, + "executeAt": "2026-05-10T09:30:00Z", + "nextRetryAt": "2026-05-10T09:35:00Z", + "consumerId": "consumer_03", + "lockedAt": "2026-05-10T09:30:05Z", + "traceId": "trace_jkl012", + "options": { "error": "SMTP connection timeout" }, + "createdAt": "2026-05-10T09:30:00Z", + "updatedAt": "2026-05-10T09:50:00Z" + }, + { + "id": "msg_005", + "topic": "order.created", + "payload": { "orderId": "ord_67890", "amount": 89.50, "customerId": "cust_002" }, + "status": "COMPLETED", + "retryCount": 0, + "maxRetries": 3, + "priority": 1, + "executeAt": "2026-05-10T10:15:00Z", + "nextRetryAt": null, + "consumerId": "consumer_01", + "lockedAt": "2026-05-10T10:15:01Z", + "traceId": "trace_mno345", + "options": {}, + "createdAt": "2026-05-10T10:15:00Z", + "updatedAt": "2026-05-10T10:15:08Z" + }, + { + "id": "msg_006", + "topic": "inventory.update", + "payload": { "sku": "PROD-001", "quantity": -5, "warehouse": "WH-EAST" }, + "status": "PROCESSING", + "retryCount": 0, + "maxRetries": 3, + "priority": 2, + "executeAt": "2026-05-10T10:20:00Z", + "nextRetryAt": null, + "consumerId": "consumer_04", + "lockedAt": "2026-05-10T10:20:01Z", + "traceId": "trace_pqr678", + "options": {}, + "createdAt": "2026-05-10T10:20:00Z", + "updatedAt": "2026-05-10T10:20:10Z" + }, + { + "id": "msg_007", + "topic": "notification.push", + "payload": { "userId": "user_111", "title": "New Order", "body": "Your order has been shipped" }, + "status": "PENDING", + "retryCount": 0, + "maxRetries": 3, + "priority": 1, + "executeAt": "2026-05-10T10:25:00Z", + "nextRetryAt": null, + "consumerId": null, + "lockedAt": null, + "traceId": "trace_stu901", + "options": {}, + "createdAt": "2026-05-10T10:25:00Z", + "updatedAt": "2026-05-10T10:25:00Z" + }, + { + "id": "msg_008", + "topic": "file.process", + "payload": { "fileId": "file_555", "type": "image", "operations": ["resize", "compress"] }, + "status": "FAILED", + "retryCount": 2, + "maxRetries": 3, + "priority": 0, + "executeAt": "2026-05-10T09:00:00Z", + "nextRetryAt": "2026-05-10T09:10:00Z", + "consumerId": "consumer_05", + "lockedAt": "2026-05-10T09:00:05Z", + "traceId": "trace_vwx234", + "options": { "error": "File too large" }, + "createdAt": "2026-05-10T09:00:00Z", + "updatedAt": "2026-05-10T09:40:00Z" + } + ], + "totalCount": 8 +} diff --git a/src/mockPreset/trace-list.json b/src/mockPreset/trace-list.json new file mode 100644 index 0000000..8285a4a --- /dev/null +++ b/src/mockPreset/trace-list.json @@ -0,0 +1,86 @@ +{ + "pageData": [ + { + "id": "trace_001", + "traceId": "trace_abc123", + "topic": "order.created", + "event": "PUBLISHED", + "detail": { "orderId": "ord_12345" }, + "messageId": "msg_001", + "createdAt": "2026-05-10T10:00:00Z" + }, + { + "id": "trace_002", + "traceId": "trace_abc123", + "topic": "order.created", + "event": "PROCESSING", + "detail": { "consumerId": "consumer_01" }, + "messageId": "msg_001", + "createdAt": "2026-05-10T10:00:01Z" + }, + { + "id": "trace_003", + "traceId": "trace_abc123", + "topic": "order.created", + "event": "PROCESSING", + "detail": { "startTime": "2026-05-10T10:00:01Z" }, + "messageId": "msg_001", + "createdAt": "2026-05-10T10:00:01Z" + }, + { + "id": "trace_004", + "traceId": "trace_abc123", + "topic": "order.created", + "event": "COMPLETED", + "detail": { "duration": 4000, "result": "success" }, + "messageId": "msg_001", + "createdAt": "2026-05-10T10:00:05Z" + }, + { + "id": "trace_005", + "traceId": "trace_def456", + "topic": "user.registered", + "event": "PUBLISHED", + "detail": { "userId": "user_54321" }, + "messageId": "msg_002", + "createdAt": "2026-05-10T10:05:00Z" + }, + { + "id": "trace_006", + "traceId": "trace_def456", + "topic": "user.registered", + "event": "PROCESSING", + "detail": { "consumerId": "consumer_02" }, + "messageId": "msg_002", + "createdAt": "2026-05-10T10:05:01Z" + }, + { + "id": "trace_007", + "traceId": "trace_def456", + "topic": "user.registered", + "event": "PROCESSING", + "detail": { "startTime": "2026-05-10T10:05:01Z" }, + "messageId": "msg_002", + "createdAt": "2026-05-10T10:05:01Z" + }, + { + "id": "trace_008", + "traceId": "trace_def456", + "topic": "user.registered", + "event": "FAILED", + "detail": { "error": "Database connection failed", "retryCount": 1 }, + "messageId": "msg_002", + "createdAt": "2026-05-10T10:05:30Z" + }, + { + "id": "trace_009", + "traceId": "trace_def456", + "topic": "user.registered", + "event": "FAILED", + "detail": { "nextRetryAt": "2026-05-10T10:06:00Z" }, + "messageId": "msg_002", + "createdAt": "2026-05-10T10:05:30Z" + } + ], + "totalCount": 9 +} diff --git a/src/preset.js b/src/preset.js index e3bffb0..6927b81 100644 --- a/src/preset.js +++ b/src/preset.js @@ -97,7 +97,7 @@ export const globalInit = async () => { //url: 'http://localhost:3010', //tpl: '{{url}}', remote: 'components-thirdparty', - defaultVersion: '0.1.12' + defaultVersion: '0.1.17' }, 'components-admin': process.env.NODE_ENV === 'development'