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 af84b58d feat: support message tag query (#532)
af84b58d is described below
commit af84b58d52a4e4b83dfd0ff22906556ce0630e1a
Author: aias00 <[email protected]>
AuthorDate: Tue Jul 28 05:22:15 2026 -0700
feat: support message tag query (#532)
* fix: align frontend API success response handling (#429)
* ci: add build workflow and PR review skill (#430)
* fix: connect K8s certificate page to APIs (#440)
* feat: extend translation keys & add useLanguage compatible alias in
LangContext (#433)
* fix: validate audit query and cleanup parameters (#442)
* chore: fix Dockerfile missing style/ copy and standardize pr-review skill
(#444)
- Add COPY style ./style to server/Dockerfile so Maven checkstyle can
find style/rmq_checkstyle.xml during docker compose build
- Rewrite pr-review SKILL.md with an 8-stage pipeline checklist,
Dockerfile pre-flight fix step, and [Studio] title format spec
* feat: add centralized ThemeContext and useTheme hook for dark mode
management (#435)
* fix: preserve SSE framing for AI chat (#438)
* feat: add Prometheus range query adapter (#432)
Add a real Prometheus /api/v1/query_range adapter as the foundation for
observability (#431): configurable base URL, timeouts, Basic/Bearer auth, error
mapping, and tests.
* feat: implement login page, auth & AI modules, simplify theme management
(#473)
Add login page with full auth workflow, Auth/AI API modules, Zustand state,
and simplified theme management.
* feat: add Ops page (NameServer management, VIPChannel, TLS) (#474)
Add Ops management page for NameServer address management, VIP channel
toggle, and TLS switch.
* feat: add Producer page (#475)
Add a page to query producer client connections by Topic and Producer Group.
* feat: add AlertManagement page for alert rule operations (#476)
Add alert rule management page: parse/render Prometheus AlertManager YAML
rules with search, toggle, and CRUD.
* feat: add SslSettings page for SSL/TLS configuration management (#477)
Add SSL/TLS configuration page: SSL toggle, TLS version,
KeyStore/TrustStore, client auth mode.
* feat: add broker cluster overview page (#479)
Add Broker Cluster overview page with tab-based NameServer/Broker/Proxy
management and cluster stats.
* feat: add consumer group management page (#480)
Add consumer group management page.
* feat: add lite topic management page (#481)
Add lite topic management page.
* feat: add Proxy management page (#482)
Add Proxy management page (address list, status, TPS, memory, CPU, uptime).
* feat: add LLM Settings configuration page (#483)
Add LLM settings configuration page (provider, apiKey, model, temperature,
etc.).
* feat: connect frontend pages to backend APIs and align contracts (#445)
Consolidated API-integration work
(#445,#446,#447,#448,#449,#450,#451,#452,#453,#454,#458,#459,#460,#461,#463).
* feat: add service capabilities and interaction enhancements (#462)
Consolidated service/interaction work
(#462,#468,#469,#464,#470,#471,#472,#488).
* feat: add preferences persistence, accessibility and env config (#465)
Consolidated preferences/a11y/config work (#465,#466,#485,#486,#487,#484).
* fix: include remaining consolidated frontend changes (#490)
topicService topic API contract, authStore persisted session, vite env
config.
* fix: align login response contract (#492)
Align frontend LoginResponse type with backend LoginVO contract: change
flat username/role to nested user.username/admin structure.
* fix: use runtime DNS resolver for Nginx API proxy (#494)
Fix Nginx API proxy failure caused by hardcoded DNS resolver by switching
to container runtime dynamic resolution.
* feat: enhance i18n support with labelKey-based translations (#495)
Migrate hardcoded Chinese UI text to i18n labelKey pattern, add ~280
translation entries (zh+en), fix duplicate key compilation errors.
* test: fix frontend baseline assertions (#498)
Fix test assertions to align with backend PageResult contract and current
i18n labels.
* fix: initialize deploy helpers before validation (#499)
Move logging/error helper initialization before config validation in
deploy.sh to fix err() undefined when REMOTE_HOST is missing.
* ci: build frontend Docker image (#500)
Add frontend Docker image build to CI workflow to validate Dockerfile/nginx
deployment path.
* fix: load producer topics from Studio API response (#501)
Fix Producer page topic list to read from Studio API new format
(data[].name) with backward compatibility for legacy topicList format.
* fix: generate data source keys on creation (#509)
Fix ConcurrentHashMap NPE when creating data sources with null key by
auto-generating UUID in service layer.
* feat: add producer connection endpoint (#510)
Add producer connection query endpoint with topic/producerGroup filtering,
returning backward-compatible connectionSet format.
* fix: avoid returning General Settings API key (#512)
Fix security issue where General Settings endpoint returned API key in
plaintext. Replace with apiKeyConfigured boolean flag and support explicit key
clearing.
* feat: add catalog-driven read-only tools (#514)
Introduce versioned YAML tool catalog with JSON Schema validation, SHA-256
digest, deep immutability, and two read-only L1 tool handlers (rmq.cluster.list
/ rmq.capabilities).
* feat: add LiteTopic backend endpoints (#502)
Add backend stub endpoints for LiteTopic page: list, session details, TTL
extension, quota, and capability check.
* feat: add ops backend endpoints (#504)
Add ops page backend endpoints: NameServer address management, VIP Channel,
and TLS switches.
* feat: add proxy address endpoints (#505)
Add proxy address list and add-address endpoints supporting Studio frontend
form submission.
* feat: add LLM settings endpoints (#506)
Add LLM settings backend endpoints: config read/write, connectivity test,
and model listing.
* feat: add alert rules YAML endpoint (#507)
Add alert rules Prometheus YAML export endpoint with default RocketMQ alert
template fallback.
* fix: ACL page API integration (#516)
Wire ACL management page to backend API, add update endpoints for ACL rules
and users with proper validation.
* fix: sort queried messages by store time (#519)
Sort message query results by storeTime in descending order so latest
messages appear first.
* feat: add consumer stack diagnostics API (#521)
Add consumer thread stack diagnostics endpoint (GET
/api/groups/{name}/instances/{clientId}/stack) with Provider/Stub/Service
layering.
* feat: support message tag query
---------
Co-authored-by: btlqql <[email protected]>
Co-authored-by: lizhimins <[email protected]>
Co-authored-by: zhaohai <[email protected]>
Co-authored-by: terrancesli <[email protected]>
Co-authored-by: wizcraft_kris
<[email protected]>
Co-authored-by: Loyal-Young <[email protected]>
Co-authored-by: majialong <[email protected]>
Co-authored-by: yx9o <[email protected]>
Co-authored-by: PiliLily <[email protected]>
---
.../studio/instance/message/MessageController.java | 3 +-
.../studio/instance/message/MessageProvider.java | 3 +-
.../instance/message/MessageProviderStub.java | 3 +-
.../studio/instance/message/MessageService.java | 7 ++-
.../instance/message/MessageControllerTest.java | 72 ++++++++++++++++++++++
web/src/api/message.test.ts | 1 +
web/src/api/message.ts | 1 +
web/src/pages/instance/message.tsx | 12 +++-
web/src/services/messageService.ts | 1 +
9 files changed, 96 insertions(+), 7 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageController.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageController.java
index e8dfe10b..674e166e 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageController.java
@@ -37,10 +37,11 @@ public class MessageController {
public Result<List<MessageRecordVO>> queryMessages(
@RequestParam(required = false) String topic,
@RequestParam(required = false) String msgId,
+ @RequestParam(required = false) String tag,
@RequestParam(required = false) String key,
@RequestParam(required = false) Long startTime,
@RequestParam(required = false) Long endTime) {
- return Result.ok(messageService.queryMessages(topic, msgId, key,
startTime, endTime));
+ return Result.ok(messageService.queryMessages(topic, msgId, tag, key,
startTime, endTime));
}
@GetMapping("/{msgId}/trace")
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProvider.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProvider.java
index 78c0147b..3b3b5c0b 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProvider.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProvider.java
@@ -20,7 +20,8 @@ package org.apache.rocketmq.studio.instance.message;
import java.util.List;
public interface MessageProvider {
- List<MessageRecordVO> queryMessages(String topic, String msgId, String
key, Long startTime, Long endTime);
+ List<MessageRecordVO> queryMessages(String topic, String msgId, String
tag, String key, Long startTime,
+ Long endTime);
TraceRecordVO getMessageTrace(String msgId);
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProviderStub.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProviderStub.java
index a5b1b1ab..db3f04c4 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProviderStub.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageProviderStub.java
@@ -27,7 +27,8 @@ import java.util.List;
public class MessageProviderStub implements MessageProvider {
@Override
- public List<MessageRecordVO> queryMessages(String topic, String msgId,
String key, Long startTime, Long endTime) {
+ public List<MessageRecordVO> queryMessages(String topic, String msgId,
String tag, String key, Long startTime,
+ Long endTime) {
log.warn("MessageProviderStub.queryMessages called - returning empty
list");
return Collections.emptyList();
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
index 4f1997c0..a2bdafd2 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/MessageService.java
@@ -29,9 +29,10 @@ public class MessageService {
private final MessageProvider messageProvider;
- public List<MessageRecordVO> queryMessages(String topic, String msgId,
String key, Long startTime, Long endTime) {
- log.info("Querying messages: topic={}, msgId={}, key={}", topic,
msgId, key);
- return messageProvider.queryMessages(topic, msgId, key, startTime,
endTime);
+ public List<MessageRecordVO> queryMessages(
+ String topic, String msgId, String tag, String key, Long
startTime, Long endTime) {
+ log.info("Querying messages: topic={}, msgId={}, tag={}, key={}",
topic, msgId, tag, key);
+ return messageProvider.queryMessages(topic, msgId, tag, key,
startTime, endTime);
}
public TraceRecordVO getMessageTrace(String msgId) {
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageControllerTest.java
new file mode 100644
index 00000000..49278190
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/MessageControllerTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.
+ */
+
+package org.apache.rocketmq.studio.instance.message;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(MessageController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class MessageControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private MessageService messageService;
+
+ @Test
+ void queryMessagesShouldPassTagFilter() throws Exception {
+ MessageRecordVO message = MessageRecordVO.builder()
+ .msgId("msg-001")
+ .topic("orders")
+ .tag("created")
+ .key("order-1")
+ .build();
+ when(messageService.queryMessages(eq("orders"), isNull(),
eq("created"), eq("order-1"),
+ eq(1784246400000L),
eq(1784332800000L))).thenReturn(List.of(message));
+
+ mockMvc.perform(get("/api/messages")
+ .param("topic", "orders")
+ .param("tag", "created")
+ .param("key", "order-1")
+ .param("startTime", "1784246400000")
+ .param("endTime", "1784332800000"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data[0].msgId").value("msg-001"))
+ .andExpect(jsonPath("$.data[0].tag").value("created"));
+
+ verify(messageService).queryMessages(eq("orders"), isNull(),
eq("created"), eq("order-1"),
+ eq(1784246400000L), eq(1784332800000L));
+ }
+}
diff --git a/web/src/api/message.test.ts b/web/src/api/message.test.ts
index 5119a1a5..b435b4e8 100644
--- a/web/src/api/message.test.ts
+++ b/web/src/api/message.test.ts
@@ -36,6 +36,7 @@ describe('message API', () => {
it('sends the backend-supported query fields with epoch timestamps', async
() => {
const params = {
topic: 'orders',
+ tag: 'created',
key: 'order-1',
startTime: 1784246400000,
endTime: 1784332800000,
diff --git a/web/src/api/message.ts b/web/src/api/message.ts
index 716c387c..39a0da03 100644
--- a/web/src/api/message.ts
+++ b/web/src/api/message.ts
@@ -36,6 +36,7 @@ export interface TraceRecord {
export interface MessageQuery {
topic?: string;
+ tag?: string;
key?: string;
msgId?: string;
startTime?: number;
diff --git a/web/src/pages/instance/message.tsx
b/web/src/pages/instance/message.tsx
index 73d9bb66..553a0d10 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -119,6 +119,7 @@ const MessagePage = () => {
const [queryMode, setQueryMode] = useState<QueryMode>('topic');
const [selectedTopic, setSelectedTopic] = useState<string | undefined>();
const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>(getDefaultRange);
+ const [tagInput, setTagInput] = useState('');
const [keyInput, setKeyInput] = useState('');
const [msgIdInput, setMsgIdInput] = useState('');
const [messages, setMessages] = useState<MessageRecord[]>([]);
@@ -132,6 +133,7 @@ const MessagePage = () => {
/* ─── Handlers ─── */
const handleReset = () => {
setSelectedTopic(undefined);
+ setTagInput('');
setKeyInput('');
setMsgIdInput('');
setDateRange(getDefaultRange());
@@ -143,6 +145,7 @@ const MessagePage = () => {
queryMode === 'topic'
? {
topic: selectedTopic,
+ tag: tagInput || undefined,
startTime: dateRange[0].valueOf(),
endTime: dateRange[1].valueOf(),
}
@@ -447,7 +450,7 @@ const MessagePage = () => {
═══════════════════════════════════════════ */
return (
<div style={{ padding: 24 }}>
- <PageHeader title={t('message.title')} subtitle="按 Topic、Key 或 Message
ID 检索消息" />
+ <PageHeader title={t('message.title')} subtitle="按 Topic、Tag、Key 或
Message ID 检索消息" />
{/* ── Query Form ── */}
<Card style={{ marginBottom: 16 }}>
@@ -483,6 +486,13 @@ const MessagePage = () => {
}
}}
/>
+ <Input
+ placeholder="输入 Tag(可选)"
+ style={{ width: 180 }}
+ value={tagInput}
+ onChange={(e) => setTagInput(e.target.value)}
+ allowClear
+ />
</>
)}
diff --git a/web/src/services/messageService.ts
b/web/src/services/messageService.ts
index 81896b59..852b40d1 100644
--- a/web/src/services/messageService.ts
+++ b/web/src/services/messageService.ts
@@ -9,6 +9,7 @@ export async function queryMessages(params: MessageQuery):
Promise<MessageRecord
if (USE_MOCK) {
let result = [...mockMessages];
if (params.topic) result = result.filter((m) => m.topic === params.topic);
+ if (params.tag) result = result.filter((m) => m.tag === params.tag);
if (params.key) result = result.filter((m) => m.key.includes(params.key!));
if (params.msgId) result = result.filter((m) => m.msgId === params.msgId);
return sortMessagesByStoreTimeDesc(result as unknown as MessageRecord[]);