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 = () => {

Reply via email to