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

commit f14471f7b80bb11cce1b4d927177e30363d8d003
Author: aias00 <[email protected]>
AuthorDate: Thu Jul 23 22:51:05 2026 -0700

    feat: add ops backend endpoints (#504)
    
    Add ops page backend endpoints: NameServer address management, VIP Channel, 
and TLS switches.
---
 .../com/rocketmq/studio/ops/OpsController.java     |  63 ++++++++++++
 .../java/com/rocketmq/studio/ops/OpsHomeVO.java    |  32 ++++++
 .../com/rocketmq/studio/ops/OpsNameServerDTO.java  |  25 +++++
 .../java/com/rocketmq/studio/ops/OpsService.java   |  76 ++++++++++++++
 .../java/com/rocketmq/studio/ops/OpsTlsDTO.java    |  25 +++++
 .../com/rocketmq/studio/ops/OpsVipChannelDTO.java  |  25 +++++
 .../com/rocketmq/studio/ops/OpsControllerTest.java | 112 +++++++++++++++++++++
 .../com/rocketmq/studio/ops/OpsServiceTest.java    |  72 +++++++++++++
 8 files changed, 430 insertions(+)

diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsController.java 
b/server/src/main/java/com/rocketmq/studio/ops/OpsController.java
new file mode 100644
index 00000000..5bfb8a23
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/OpsController.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.rocketmq.studio.ops;
+
+import com.rocketmq.studio.common.domain.Result;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/api/ops")
+@RequiredArgsConstructor
+public class OpsController {
+
+    private final OpsService opsService;
+
+    @GetMapping("/homePage")
+    public Result<OpsHomeVO> homePage() {
+        return Result.ok(opsService.getHomePage());
+    }
+
+    @PostMapping("/updateNameSvrAddr")
+    public Result<Void> updateNameSvrAddr(@RequestBody OpsNameServerDTO 
request) {
+        opsService.updateNameServer(request.getNamesrvAddr());
+        return Result.ok();
+    }
+
+    @PostMapping("/addNameSvrAddr")
+    public Result<Void> addNameSvrAddr(@RequestBody OpsNameServerDTO request) {
+        opsService.addNameServer(request.getNamesrvAddr());
+        return Result.ok();
+    }
+
+    @PostMapping("/updateIsVIPChannel")
+    public Result<Void> updateIsVIPChannel(@RequestBody OpsVipChannelDTO 
request) {
+        opsService.updateVipChannel(request.isUseVIPChannel());
+        return Result.ok();
+    }
+
+    @PostMapping("/updateUseTLS")
+    public Result<Void> updateUseTLS(@RequestBody OpsTlsDTO request) {
+        opsService.updateUseTLS(request.isUseTLS());
+        return Result.ok();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsHomeVO.java 
b/server/src/main/java/com/rocketmq/studio/ops/OpsHomeVO.java
new file mode 100644
index 00000000..ec7893a1
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/OpsHomeVO.java
@@ -0,0 +1,32 @@
+/*
+ * 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 com.rocketmq.studio.ops;
+
+import lombok.Builder;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Builder
+public class OpsHomeVO {
+    private List<String> namesvrAddrList;
+    private boolean useVIPChannel;
+    private boolean useTLS;
+    private String currentNamesrv;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsNameServerDTO.java 
b/server/src/main/java/com/rocketmq/studio/ops/OpsNameServerDTO.java
new file mode 100644
index 00000000..a520cd75
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/OpsNameServerDTO.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.ops;
+
+import lombok.Data;
+
+@Data
+public class OpsNameServerDTO {
+    private String namesrvAddr;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsService.java 
b/server/src/main/java/com/rocketmq/studio/ops/OpsService.java
new file mode 100644
index 00000000..aa7e7123
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/OpsService.java
@@ -0,0 +1,76 @@
+/*
+ * 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 com.rocketmq.studio.ops;
+
+import com.rocketmq.studio.common.exception.BusinessException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+@Slf4j
+@Service
+public class OpsService {
+
+    private final Set<String> namesrvAddrs = new 
LinkedHashSet<>(List.of("127.0.0.1:9876"));
+    private String currentNamesrv = "127.0.0.1:9876";
+    private boolean useVIPChannel = true;
+    private boolean useTLS;
+
+    public synchronized OpsHomeVO getHomePage() {
+        return OpsHomeVO.builder()
+                .namesvrAddrList(new ArrayList<>(namesrvAddrs))
+                .currentNamesrv(currentNamesrv)
+                .useVIPChannel(useVIPChannel)
+                .useTLS(useTLS)
+                .build();
+    }
+
+    public synchronized void updateNameServer(String namesrvAddr) {
+        String normalized = normalizeNameServer(namesrvAddr);
+        namesrvAddrs.add(normalized);
+        currentNamesrv = normalized;
+        log.info("Updated current NameServer address to {}", normalized);
+    }
+
+    public synchronized void addNameServer(String namesrvAddr) {
+        String normalized = normalizeNameServer(namesrvAddr);
+        namesrvAddrs.add(normalized);
+        log.info("Added NameServer address {}", normalized);
+    }
+
+    public synchronized void updateVipChannel(boolean enabled) {
+        useVIPChannel = enabled;
+        log.info("Updated VIP channel setting to {}", enabled);
+    }
+
+    public synchronized void updateUseTLS(boolean enabled) {
+        useTLS = enabled;
+        log.info("Updated TLS setting to {}", enabled);
+    }
+
+    private String normalizeNameServer(String namesrvAddr) {
+        if (namesrvAddr == null || namesrvAddr.trim().isEmpty()) {
+            throw new BusinessException(400, "namesrvAddr is required");
+        }
+        return namesrvAddr.trim();
+    }
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsTlsDTO.java 
b/server/src/main/java/com/rocketmq/studio/ops/OpsTlsDTO.java
new file mode 100644
index 00000000..1ed42ee2
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/OpsTlsDTO.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.ops;
+
+import lombok.Data;
+
+@Data
+public class OpsTlsDTO {
+    private boolean useTLS;
+}
diff --git a/server/src/main/java/com/rocketmq/studio/ops/OpsVipChannelDTO.java 
b/server/src/main/java/com/rocketmq/studio/ops/OpsVipChannelDTO.java
new file mode 100644
index 00000000..612b8f38
--- /dev/null
+++ b/server/src/main/java/com/rocketmq/studio/ops/OpsVipChannelDTO.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.rocketmq.studio.ops;
+
+import lombok.Data;
+
+@Data
+public class OpsVipChannelDTO {
+    private boolean useVIPChannel;
+}
diff --git 
a/server/src/test/java/com/rocketmq/studio/ops/OpsControllerTest.java 
b/server/src/test/java/com/rocketmq/studio/ops/OpsControllerTest.java
new file mode 100644
index 00000000..cbc4582b
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/ops/OpsControllerTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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 com.rocketmq.studio.ops;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+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.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.mockito.ArgumentMatchers.eq;
+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.request.MockMvcRequestBuilders.post;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static 
org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@WebMvcTest(OpsController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class OpsControllerTest {
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    @Autowired
+    private ObjectMapper objectMapper;
+
+    @MockBean
+    private OpsService opsService;
+
+    @Test
+    void homePageShouldReturnOpsSettings() throws Exception {
+        OpsHomeVO home = OpsHomeVO.builder()
+                .namesvrAddrList(List.of("127.0.0.1:9876", "10.0.0.1:9876"))
+                .currentNamesrv("127.0.0.1:9876")
+                .useVIPChannel(true)
+                .useTLS(false)
+                .build();
+        when(opsService.getHomePage()).thenReturn(home);
+
+        mockMvc.perform(get("/api/ops/homePage"))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200))
+                
.andExpect(jsonPath("$.data.namesvrAddrList[0]").value("127.0.0.1:9876"))
+                
.andExpect(jsonPath("$.data.currentNamesrv").value("127.0.0.1:9876"))
+                .andExpect(jsonPath("$.data.useVIPChannel").value(true))
+                .andExpect(jsonPath("$.data.useTLS").value(false));
+    }
+
+    @Test
+    void updateNameSvrAddrShouldDelegateToService() throws Exception {
+        mockMvc.perform(post("/api/ops/updateNameSvrAddr")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        
.content(objectMapper.writeValueAsString(Map.of("namesrvAddr", 
"10.0.0.1:9876"))))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200));
+
+        verify(opsService).updateNameServer(eq("10.0.0.1:9876"));
+    }
+
+    @Test
+    void addNameSvrAddrShouldDelegateToService() throws Exception {
+        mockMvc.perform(post("/api/ops/addNameSvrAddr")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        
.content(objectMapper.writeValueAsString(Map.of("namesrvAddr", 
"10.0.0.2:9876"))))
+                .andExpect(status().isOk());
+
+        verify(opsService).addNameServer(eq("10.0.0.2:9876"));
+    }
+
+    @Test
+    void updateVipChannelShouldDelegateToService() throws Exception {
+        mockMvc.perform(post("/api/ops/updateIsVIPChannel")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        
.content(objectMapper.writeValueAsString(Map.of("useVIPChannel", false))))
+                .andExpect(status().isOk());
+
+        verify(opsService).updateVipChannel(false);
+    }
+
+    @Test
+    void updateUseTlsShouldDelegateToService() throws Exception {
+        mockMvc.perform(post("/api/ops/updateUseTLS")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        
.content(objectMapper.writeValueAsString(Map.of("useTLS", true))))
+                .andExpect(status().isOk());
+
+        verify(opsService).updateUseTLS(true);
+    }
+}
diff --git a/server/src/test/java/com/rocketmq/studio/ops/OpsServiceTest.java 
b/server/src/test/java/com/rocketmq/studio/ops/OpsServiceTest.java
new file mode 100644
index 00000000..dd2dc23c
--- /dev/null
+++ b/server/src/test/java/com/rocketmq/studio/ops/OpsServiceTest.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 com.rocketmq.studio.ops;
+
+import com.rocketmq.studio.common.exception.BusinessException;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class OpsServiceTest {
+
+    private final OpsService opsService = new OpsService();
+
+    @Test
+    void getHomePageShouldReturnDefaultSettings() {
+        OpsHomeVO home = opsService.getHomePage();
+
+        
assertThat(home.getNamesvrAddrList()).containsExactly("127.0.0.1:9876");
+        assertThat(home.getCurrentNamesrv()).isEqualTo("127.0.0.1:9876");
+        assertThat(home.isUseVIPChannel()).isTrue();
+        assertThat(home.isUseTLS()).isFalse();
+    }
+
+    @Test
+    void addAndUpdateNameServerShouldMaintainUniqueAddressList() {
+        opsService.addNameServer(" 10.0.0.1:9876 ");
+        opsService.addNameServer("10.0.0.1:9876");
+        opsService.updateNameServer("10.0.0.2:9876");
+
+        OpsHomeVO home = opsService.getHomePage();
+        assertThat(home.getNamesvrAddrList())
+                .containsExactly("127.0.0.1:9876", "10.0.0.1:9876", 
"10.0.0.2:9876");
+        assertThat(home.getCurrentNamesrv()).isEqualTo("10.0.0.2:9876");
+    }
+
+    @Test
+    void togglesShouldUpdateHomePageSettings() {
+        opsService.updateVipChannel(false);
+        opsService.updateUseTLS(true);
+
+        OpsHomeVO home = opsService.getHomePage();
+        assertThat(home.isUseVIPChannel()).isFalse();
+        assertThat(home.isUseTLS()).isTrue();
+    }
+
+    @Test
+    void nameServerOperationsShouldRejectBlankAddress() {
+        assertThatThrownBy(() -> opsService.addNameServer(" "))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("namesrvAddr is required")
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400));
+        assertThatThrownBy(() -> opsService.updateNameServer(null))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("namesrvAddr is required");
+    }
+}

Reply via email to