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 ab85dd95 feat: add proxy address removal flow (#577)
ab85dd95 is described below
commit ab85dd952e04ca05a6b213134ae8eb6415918c73
Author: aias00 <[email protected]>
AuthorDate: Tue Jul 28 05:34:07 2026 -0700
feat: add proxy address removal flow (#577)
---
.../studio/cluster/proxy/ProxyAddressService.java | 17 +++++++--
.../cluster/proxy/ProxyCompatController.java | 6 +++
.../cluster/proxy/ProxyAddressServiceTest.java | 44 ++++++++++++++++++++++
.../cluster/proxy/ProxyCompatControllerTest.java | 11 ++++++
web/src/api/proxy.test.ts | 12 +++++-
web/src/api/proxy.ts | 8 ++++
web/src/i18n/translations.ts | 1 +
web/src/pages/studio/Proxy.tsx | 15 +++++++-
8 files changed, 108 insertions(+), 6 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
index 592baf35..5495a573 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressService.java
@@ -41,7 +41,7 @@ public class ProxyAddressService {
}
public synchronized void addProxyAddr(String newProxyAddr) {
- String normalized = normalizeProxyAddr(newProxyAddr);
+ String normalized = normalizeProxyAddr(newProxyAddr, "newProxyAddr");
proxyAddrs.add(normalized);
if (currentProxyAddr == null || currentProxyAddr.isBlank()) {
currentProxyAddr = normalized;
@@ -49,9 +49,20 @@ public class ProxyAddressService {
log.info("Added Proxy address {}", normalized);
}
- private String normalizeProxyAddr(String proxyAddr) {
+ public synchronized void removeProxyAddr(String proxyAddr) {
+ String normalized = normalizeProxyAddr(proxyAddr, "proxyAddr");
+ if (!proxyAddrs.remove(normalized)) {
+ throw new BusinessException(404, "Proxy address not found: " +
normalized);
+ }
+ if (normalized.equals(currentProxyAddr)) {
+ currentProxyAddr = proxyAddrs.stream().findFirst().orElse("");
+ }
+ log.info("Removed Proxy address {}", normalized);
+ }
+
+ private String normalizeProxyAddr(String proxyAddr, String fieldName) {
if (proxyAddr == null || proxyAddr.trim().isEmpty()) {
- throw new BusinessException(400, "newProxyAddr is required");
+ throw new BusinessException(400, fieldName + " is required");
}
return proxyAddr.trim();
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatController.java
b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatController.java
index 37270915..84d2e1a5 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatController.java
@@ -43,4 +43,10 @@ public class ProxyCompatController {
proxyAddressService.addProxyAddr(newProxyAddr);
return Result.ok();
}
+
+ @PostMapping(value = "/removeProxyAddr.do", consumes =
MediaType.APPLICATION_FORM_URLENCODED_VALUE)
+ public Result<Void> removeProxyAddr(@RequestParam String proxyAddr) {
+ proxyAddressService.removeProxyAddr(proxyAddr);
+ return Result.ok();
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressServiceTest.java
index 64244046..81a647b2 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyAddressServiceTest.java
@@ -52,4 +52,48 @@ class ProxyAddressServiceTest {
.hasMessage("newProxyAddr is required")
.satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(400));
}
+
+ @Test
+ void removeProxyAddrShouldTrimAndRemoveAddress() {
+ proxyAddressService.addProxyAddr("10.0.0.1:8081");
+
+ proxyAddressService.removeProxyAddr(" 10.0.0.1:8081 ");
+
+ ProxyHomeVO home = proxyAddressService.getHomePage();
+ assertThat(home.getProxyAddrList()).containsExactly("127.0.0.1:8081");
+ assertThat(home.getCurrentProxyAddr()).isEqualTo("127.0.0.1:8081");
+ }
+
+ @Test
+ void removeProxyAddrShouldSelectNextProxyWhenCurrentIsRemoved() {
+ proxyAddressService.removeProxyAddr("127.0.0.1:8081");
+
+ ProxyHomeVO emptyHome = proxyAddressService.getHomePage();
+ assertThat(emptyHome.getProxyAddrList()).isEmpty();
+ assertThat(emptyHome.getCurrentProxyAddr()).isEmpty();
+
+ proxyAddressService.addProxyAddr("10.0.0.1:8081");
+ proxyAddressService.addProxyAddr("10.0.0.2:8081");
+ proxyAddressService.removeProxyAddr("10.0.0.1:8081");
+
+ ProxyHomeVO home = proxyAddressService.getHomePage();
+ assertThat(home.getProxyAddrList()).containsExactly("10.0.0.2:8081");
+ assertThat(home.getCurrentProxyAddr()).isEqualTo("10.0.0.2:8081");
+ }
+
+ @Test
+ void removeProxyAddrShouldRejectBlankAddress() {
+ assertThatThrownBy(() -> proxyAddressService.removeProxyAddr(" "))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("proxyAddr is required")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(400));
+ }
+
+ @Test
+ void removeProxyAddrShouldRejectUnknownAddress() {
+ assertThatThrownBy(() ->
proxyAddressService.removeProxyAddr("10.0.0.1:8081"))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("Proxy address not found: 10.0.0.1:8081")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(404));
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatControllerTest.java
index d36ab660..dfa439bb 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatControllerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/cluster/proxy/ProxyCompatControllerTest.java
@@ -70,4 +70,15 @@ class ProxyCompatControllerTest {
verify(proxyAddressService).addProxyAddr(eq("10.0.0.1:8081"));
}
+
+ @Test
+ void removeProxyAddrShouldAcceptFormEncodedPayload() throws Exception {
+ mockMvc.perform(post("/api/proxy/removeProxyAddr.do")
+ .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+ .param("proxyAddr", "10.0.0.1:8081"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200));
+
+ verify(proxyAddressService).removeProxyAddr(eq("10.0.0.1:8081"));
+ }
}
diff --git a/web/src/api/proxy.test.ts b/web/src/api/proxy.test.ts
index 3defd5b1..e2b7f364 100644
--- a/web/src/api/proxy.test.ts
+++ b/web/src/api/proxy.test.ts
@@ -18,7 +18,7 @@
import MockAdapter from 'axios-mock-adapter';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import client from './client';
-import { queryProxyHomePage, addProxyAddr } from './proxy';
+import { queryProxyHomePage, addProxyAddr, removeProxyAddr } from './proxy';
const mock = new MockAdapter(client);
@@ -73,4 +73,14 @@ describe('Proxy API', () => {
await addProxyAddr('localhost:8081');
});
+
+ it('removes a proxy address with form-urlencoded content type', async () => {
+ mock.onPost('/proxy/removeProxyAddr.do').reply((config) => {
+
expect(config.headers?.['Content-Type']).toBe('application/x-www-form-urlencoded');
+ expect(config.data).toBe('proxyAddr=192.168.1.3%3A8081');
+ return [200, { code: 200 }];
+ });
+
+ await removeProxyAddr('192.168.1.3:8081');
+ });
});
diff --git a/web/src/api/proxy.ts b/web/src/api/proxy.ts
index 9342f61d..26e311cc 100644
--- a/web/src/api/proxy.ts
+++ b/web/src/api/proxy.ts
@@ -51,3 +51,11 @@ export async function addProxyAddr(address: string):
Promise<void> {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
});
}
+
+export async function removeProxyAddr(address: string): Promise<void> {
+ const params = new URLSearchParams();
+ params.append('proxyAddr', address);
+ await client.post('/proxy/removeProxyAddr.do', params, {
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
+ });
+}
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 3b37ff10..90d94f32 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -881,6 +881,7 @@ const translations: Record<string, Record<Lang, string>> = {
'proxy.action': { zh: '操作', en: 'Action' },
'proxy.fetchListFailed': { zh: '获取代理列表失败', en: 'Failed to fetch proxy list'
},
'proxy.addFailed': { zh: '添加代理失败', en: 'Failed to add proxy' },
+ 'proxy.removeFailed': { zh: '移除代理失败', en: 'Failed to remove proxy' },
'proxy.addrRequired': { zh: '请输入代理地址', en: 'Proxy address is required' },
'proxy.noConfigData': { zh: '无配置数据', en: 'No config data' },
'proxy.version': { zh: '版本', en: 'Version' },
diff --git a/web/src/pages/studio/Proxy.tsx b/web/src/pages/studio/Proxy.tsx
index d60be56a..bdbae56c 100644
--- a/web/src/pages/studio/Proxy.tsx
+++ b/web/src/pages/studio/Proxy.tsx
@@ -48,7 +48,7 @@ import {
} from '@phosphor-icons/react';
import PageHeader from '../../components/PageHeader';
import { useLang } from '../../i18n/LangContext';
-import { queryProxyHomePage, addProxyAddr, type ProxyNode } from
'../../api/proxy';
+import { queryProxyHomePage, addProxyAddr, removeProxyAddr, type ProxyNode }
from '../../api/proxy';
const ProxyPage: React.FC = () => {
const { t } = useLang();
@@ -161,7 +161,18 @@ const ProxyPage: React.FC = () => {
};
const handleRemoveNode = (node: ProxyNode) => {
- message.info(`Remove node operation (not implemented in API):
${node.address}`);
+ setLoading(true);
+ removeProxyAddr(node.address)
+ .then(() => {
+ message.success(t('common.success'));
+ loadProxyNodes();
+ })
+ .catch(() => {
+ message.error(t('proxy.removeFailed'));
+ })
+ .finally(() => {
+ setLoading(false);
+ });
};
const handleRefresh = () => {