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[]);

Reply via email to