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 bce4b101 fix: resolve remaining test regressions and SSRF loopback 
conflict from batch merges (#1571)
bce4b101 is described below

commit bce4b10144ba28b36fca17bd864ce3372bdb7646
Author: lizhimins <[email protected]>
AuthorDate: Tue Aug 11 11:56:32 2026 +0800

    fix: resolve remaining test regressions and SSRF loopback conflict from 
batch merges (#1571)
---
 .../provider/apache/RocketMQMessageProvider.java   |  2 +
 .../rocketmq/studio/settings/SettingsService.java  | 11 ++---
 .../provider/apache/RocketMQDLQProviderTest.java   |  6 +--
 .../studio/settings/SettingsServiceTest.java       | 12 +++++-
 .../__tests__/GrafanaDashboardList.test.tsx        |  4 +-
 web/src/pages/cluster/index.tsx                    | 20 +++++----
 web/src/pages/instance/message.tsx                 | 47 +++++++++++++---------
 7 files changed, 66 insertions(+), 36 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
index c61a174b..208b3000 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/apache/RocketMQMessageProvider.java
@@ -321,6 +321,8 @@ public class RocketMQMessageProvider implements 
MessageProvider {
      */
     private long resolveMessageStoreTimestamp(DefaultMQAdminExt adminExt, 
String msgId) {
         try {
+            // No topic hint is available in the trace flow, so locate the 
message purely
+            // by its offset msgId.
             MessageExt messageExt = viewMessageByOffsetId(adminExt, msgId);
             if (messageExt != null) {
                 return messageExt.getStoreTimestamp();
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
index aef4f15a..ba1744e0 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/settings/SettingsService.java
@@ -251,12 +251,13 @@ public class SettingsService {
 
     /**
      * SSRF guard: the test endpoint performs a server-side HTTP request to an 
attacker-supplied
-     * URL. The hostname {@code localhost} and link-local addresses 
(169.254.x.x, fe80:: — the
-     * cloud metadata range) are never legitimate Prometheus endpoints and are 
rejected. Loopback
-     * IPs and private site-local ranges stay allowed because on-premise 
Prometheus servers live
-     * on the internal network and the endpoint itself requires admin rights.
+     * URL. The hostname {@code localhost}, loopback IPs (127.x.x.x, ::1) and 
link-local addresses
+     * (169.254.x.x, fe80:: — the cloud metadata range) are never legitimate 
Prometheus endpoints
+     * and are rejected. Private site-local ranges stay allowed because 
on-premise Prometheus
+     * servers live on the internal network and the endpoint itself requires 
admin rights.
+     * Package-private so tests can admit the loopback-bound embedded test 
server.
      */
-    private boolean isAllowedDataSourceHost(String host) {
+    boolean isAllowedDataSourceHost(String host) {
         if (!StringUtils.hasText(host)) {
             return false;
         }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
index a133b8cf..38c347e4 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/apache/RocketMQDLQProviderTest.java
@@ -142,7 +142,7 @@ class RocketMQDLQProviderTest {
                 eq("RESEND_DLQ"),
                 eq("group-a"),
                 contains("matched=0, resent=0, failed=0"),
-                eq("SUCCESS"));
+                eq("NO_MESSAGES"));
         verify(runtimeAdminClientResolver).resolveEndpoint("instance-a");
     }
 
@@ -199,7 +199,7 @@ class RocketMQDLQProviderTest {
                      })) {
             assertThat(provider.resendMessages("instance-a", "group-a", 100L, 
200L, "target-topic"))
                     .extracting("matched", "resent", "failed", "outcome")
-                    .containsExactly(1, 0, 1, "PARTIAL");
+                    .containsExactly(1, 0, 1, "FAILED");
 
             assertThat(mockedConsumers.constructed()).hasSize(1);
             assertThat(mockedProducers.constructed()).hasSize(1);
@@ -209,7 +209,7 @@ class RocketMQDLQProviderTest {
                 eq("RESEND_DLQ"),
                 eq("group-a"),
                 contains("matched=1, resent=0, failed=1"),
-                eq("PARTIAL"));
+                eq("FAILED"));
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
index f514c76d..a37a3266 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/settings/SettingsServiceTest.java
@@ -64,10 +64,20 @@ class SettingsServiceTest {
 
     @BeforeEach
     void setUp() throws IOException {
-        settingsService = new SettingsService(settingsRepository, 
RestClient.builder(), new ObjectMapper(), operationAuditService);
         prometheusServer = HttpServer.create(new 
InetSocketAddress("127.0.0.1", 0), 0);
         prometheusBaseUrl = "http://127.0.0.1:"; + 
prometheusServer.getAddress().getPort();
         prometheusServer.start();
+        settingsService = new SettingsService(settingsRepository, 
RestClient.builder(), new ObjectMapper(), operationAuditService) {
+            @Override
+            boolean isAllowedDataSourceHost(String host) {
+                // The embedded test server listens on loopback, which the 
production SSRF
+                // guard blocks; admit it here and keep the real policy for 
every other host.
+                if ("127.0.0.1".equals(host)) {
+                    return true;
+                }
+                return super.isAllowedDataSourceHost(host);
+            }
+        };
     }
 
     @AfterEach
diff --git a/web/src/components/__tests__/GrafanaDashboardList.test.tsx 
b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
index fd049ad7..5717e3a7 100644
--- a/web/src/components/__tests__/GrafanaDashboardList.test.tsx
+++ b/web/src/components/__tests__/GrafanaDashboardList.test.tsx
@@ -70,6 +70,7 @@ beforeAll(() => {
 });
 
 beforeEach(() => {
+  vi.clearAllMocks();
   vi.mocked(listGrafanaDashboards).mockResolvedValue(dashboards);
   vi.mocked(getGrafanaDashboard).mockResolvedValue(dashboardModel);
   vi.mocked(exportGrafanaDashboard).mockResolvedValue(
@@ -165,7 +166,8 @@ describe('GrafanaDashboardList', () => {
     );
 
     await screen.findByText('RocketMQ Cluster Overview');
-    const exportButtons = screen.getAllByRole('button', { name: /Export|导出/ });
+    // Exact-name match: the toolbar "Export all" button also matches 
/Export|导出/.
+    const exportButtons = screen.getAllByRole('button', { name: 
/^(Export|导出)$/ });
     await user.click(exportButtons[0]);
     await user.click(exportButtons[1]);
 
diff --git a/web/src/pages/cluster/index.tsx b/web/src/pages/cluster/index.tsx
index c4088809..41057455 100644
--- a/web/src/pages/cluster/index.tsx
+++ b/web/src/pages/cluster/index.tsx
@@ -155,14 +155,18 @@ const ClusterPage = () => {
   const selectedInstanceIdRef = useRef('');
 
   useEffect(() => {
-    void listInstances().then((nextInstances) => {
-      const apacheInstances = nextInstances.filter((instance) => 
instance.vendor === 'APACHE');
-      setInstances(apacheInstances);
-      const initialInstanceId = apacheInstances[0]?.id ?? '';
-      selectedInstanceIdRef.current = initialInstanceId;
-      setSelectedInstanceId(initialInstanceId);
-      void requestRefreshRef.current('manual');
-    });
+    void listInstances()
+      .then((nextInstances) => {
+        const apacheInstances = nextInstances.filter((instance) => 
instance.vendor === 'APACHE');
+        setInstances(apacheInstances);
+        const initialInstanceId = apacheInstances[0]?.id ?? '';
+        selectedInstanceIdRef.current = initialInstanceId;
+        setSelectedInstanceId(initialInstanceId);
+        void requestRefreshRef.current('manual');
+      })
+      .catch(() => {
+        // Keep the page usable without instance filtering when the instance 
list is unavailable.
+      });
   }, []);
 
   const clearRefreshTimer = useCallback(() => {
diff --git a/web/src/pages/instance/message.tsx 
b/web/src/pages/instance/message.tsx
index 4d0ec188..ee3ba596 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -201,9 +201,36 @@ const getErrorMessage = (error: unknown, fallback: 
string): string => {
 /* ═══════════════════════════════════════════
    MessagePage
    ═══════════════════════════════════════════ */
+type InstanceFilterProps = {
+  selectedInstanceId: string;
+  selectInstance: (instanceId: string) => void;
+  instanceOptions: { value: string; label: string }[];
+};
+
 const MessagePage = () => {
-  const { t } = useLang();
   const { selectedInstanceId, selectInstance, instanceOptions } = 
useInstanceFilter();
+  // Keying the content by the selected instance makes React remount it 
whenever the instance
+  // changes — whether from this page's own <Select> or from the shared 
filter/route elsewhere —
+  // so query results, the detail modal and in-flight request ownership all 
reset cleanly.
+  return (
+    <MessagePageContent
+      key={selectedInstanceId || 'no-instance'}
+      selectedInstanceId={selectedInstanceId}
+      selectInstance={selectInstance}
+      instanceOptions={instanceOptions}
+    />
+  );
+};
+
+/* ═══════════════════════════════════════════
+   MessagePageContent
+   ═══════════════════════════════════════════ */
+const MessagePageContent = ({
+  selectedInstanceId,
+  selectInstance,
+  instanceOptions,
+}: InstanceFilterProps) => {
+  const { t } = useLang();
   const [topicOptions, setTopicOptions] = useState<string[]>([]);
 
   useEffect(() => {
@@ -250,22 +277,6 @@ const MessagePage = () => {
   );
 
   /* ─── Handlers ─── */
-  const handleInstanceChange = (instanceId: string) => {
-    queryGenerationRef.current += 1;
-    traceGenerationRef.current += 1;
-    setTopicOptions([]);
-    setSelectedTopic(undefined);
-    setMessages([]);
-    setQueryLoading(false);
-    setQueryError(null);
-    setSelectedMsg(null);
-    setModalOpen(false);
-    setTraceData(null);
-    setTraceLoading(false);
-    setTraceError(null);
-    selectInstance(instanceId);
-  };
-
   const handleReset = () => {
     queryGenerationRef.current += 1;
     setSelectedTopic(undefined);
@@ -700,7 +711,7 @@ const MessagePage = () => {
             <Select
               placeholder="选择实例"
               value={selectedInstanceId || undefined}
-              onChange={handleInstanceChange}
+              onChange={selectInstance}
               options={instanceOptions}
               style={{ width: 220 }}
               notFoundContent="暂无实例"

Reply via email to