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="暂无实例"