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 eafbb195 fix: harden ops request validation (#632)
eafbb195 is described below
commit eafbb195b45a771e43ad5124641c4af3a465bc94
Author: aias00 <[email protected]>
AuthorDate: Tue Jul 28 06:04:21 2026 -0700
fix: harden ops request validation (#632)
* [Studio] Validate ops request inputs
* fix: refresh alert rule threshold unit
* Handle missing request parameters
---
.../common/exception/GlobalExceptionHandler.java | 8 +
.../apache/rocketmq/studio/ops/OpsController.java | 13 +-
.../rocketmq/studio/ops/OpsNameServerDTO.java | 2 +
.../org/apache/rocketmq/studio/ops/OpsTlsDTO.java | 4 +-
.../rocketmq/studio/ops/OpsVipChannelDTO.java | 4 +-
.../AcknowledgeSystemAlertDTO.java} | 15 +-
.../studio/ops/alert/AlertRuleController.java | 12 +-
.../DeleteAlertRuleDTO.java} | 15 +-
.../studio/ops/alert/SystemAlertController.java | 5 +-
.../ToggleAlertRuleDTO.java} | 19 ++-
.../AuditCleanupDTO.java} | 15 +-
.../rocketmq/studio/ops/audit/AuditController.java | 5 +-
.../exception/GlobalExceptionHandlerTest.java | 14 ++
.../rocketmq/studio/ops/OpsControllerTest.java | 49 ++++++
.../studio/ops/alert/AlertRuleControllerTest.java | 173 +++++++++++++++++++++
.../ops/alert/SystemAlertControllerTest.java | 129 +++++++++++++++
.../studio/ops/audit/AuditControllerTest.java | 160 +++++++++++++++++++
.../src/pages/ops/__tests__/alerts.test.ts | 30 +++-
.../src/pages/ops/alertRulePayload.ts | 20 ++-
web/src/pages/ops/alerts.tsx | 17 +-
20 files changed, 649 insertions(+), 60 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
b/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
index 473f45ce..05bd3a24 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandler.java
@@ -24,6 +24,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@@ -64,6 +65,13 @@ public class GlobalExceptionHandler {
return Result.error(HttpStatus.BAD_REQUEST.value(), "Invalid request
body");
}
+ @ExceptionHandler(MissingServletRequestParameterException.class)
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public Result<?>
handleMissingServletRequestParameterException(MissingServletRequestParameterException
ex) {
+ String message = ex.getParameterName() + " is required";
+ return Result.error(HttpStatus.BAD_REQUEST.value(), message);
+ }
+
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<?> handleException(Exception ex) {
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsController.java
index 808aed7f..f65901f4 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsController.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsController.java
@@ -18,6 +18,7 @@
package org.apache.rocketmq.studio.ops;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -38,26 +39,26 @@ public class OpsController {
}
@PostMapping("/updateNameSvrAddr")
- public Result<Void> updateNameSvrAddr(@RequestBody OpsNameServerDTO
request) {
+ public Result<Void> updateNameSvrAddr(@Valid @RequestBody OpsNameServerDTO
request) {
opsService.updateNameServer(request.getNamesrvAddr());
return Result.ok();
}
@PostMapping("/addNameSvrAddr")
- public Result<Void> addNameSvrAddr(@RequestBody OpsNameServerDTO request) {
+ public Result<Void> addNameSvrAddr(@Valid @RequestBody OpsNameServerDTO
request) {
opsService.addNameServer(request.getNamesrvAddr());
return Result.ok();
}
@PostMapping("/updateIsVIPChannel")
- public Result<Void> updateIsVIPChannel(@RequestBody OpsVipChannelDTO
request) {
- opsService.updateVipChannel(request.isUseVIPChannel());
+ public Result<Void> updateIsVIPChannel(@Valid @RequestBody
OpsVipChannelDTO request) {
+ opsService.updateVipChannel(request.getUseVIPChannel());
return Result.ok();
}
@PostMapping("/updateUseTLS")
- public Result<Void> updateUseTLS(@RequestBody OpsTlsDTO request) {
- opsService.updateUseTLS(request.isUseTLS());
+ public Result<Void> updateUseTLS(@Valid @RequestBody OpsTlsDTO request) {
+ opsService.updateUseTLS(request.getUseTLS());
return Result.ok();
}
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsNameServerDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsNameServerDTO.java
index 91640e2a..bd104d00 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsNameServerDTO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsNameServerDTO.java
@@ -17,9 +17,11 @@
package org.apache.rocketmq.studio.ops;
+import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class OpsNameServerDTO {
+ @NotBlank(message = "namesrvAddr is required")
private String namesrvAddr;
}
diff --git a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsTlsDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsTlsDTO.java
index abf1526d..4c0fcba3 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsTlsDTO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsTlsDTO.java
@@ -17,9 +17,11 @@
package org.apache.rocketmq.studio.ops;
+import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class OpsTlsDTO {
- private boolean useTLS;
+ @NotNull(message = "useTLS is required")
+ private Boolean useTLS;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
index 571597b0..d051c0cf 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
@@ -17,9 +17,11 @@
package org.apache.rocketmq.studio.ops;
+import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class OpsVipChannelDTO {
- private boolean useVIPChannel;
+ @NotNull(message = "useVIPChannel is required")
+ private Boolean useVIPChannel;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AcknowledgeSystemAlertDTO.java
similarity index 71%
copy from
server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/ops/alert/AcknowledgeSystemAlertDTO.java
index 571597b0..74ff40bc 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AcknowledgeSystemAlertDTO.java
@@ -14,12 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.rocketmq.studio.ops.alert;
-package org.apache.rocketmq.studio.ops;
-
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class OpsVipChannelDTO {
- private boolean useVIPChannel;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AcknowledgeSystemAlertDTO {
+ @NotBlank(message = "id is required")
+ private String id;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
index 19677e5f..25a5e9ab 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertRuleController.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.ops.alert;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -25,7 +26,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
-import java.util.Map;
@RestController
@RequestMapping("/api/alert-rules")
@@ -50,15 +50,13 @@ public class AlertRuleController {
}
@PostMapping("/toggle")
- public Result<AlertRuleVO> toggleRule(@RequestBody Map<String, Object>
request) {
- String id = (String) request.get("id");
- boolean enabled = (Boolean) request.get("enabled");
- return Result.ok(alertService.toggleRule(id, enabled));
+ public Result<AlertRuleVO> toggleRule(@Valid @RequestBody
ToggleAlertRuleDTO request) {
+ return Result.ok(alertService.toggleRule(request.getId(),
request.getEnabled()));
}
@PostMapping("/delete")
- public Result<Void> deleteRule(@RequestBody Map<String, String> request) {
- alertService.deleteRule(request.get("id"));
+ public Result<Void> deleteRule(@Valid @RequestBody DeleteAlertRuleDTO
request) {
+ alertService.deleteRule(request.getId());
return Result.ok();
}
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/DeleteAlertRuleDTO.java
similarity index 71%
copy from
server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/ops/alert/DeleteAlertRuleDTO.java
index 571597b0..0f820b4a 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/DeleteAlertRuleDTO.java
@@ -14,12 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.rocketmq.studio.ops.alert;
-package org.apache.rocketmq.studio.ops;
-
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class OpsVipChannelDTO {
- private boolean useVIPChannel;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class DeleteAlertRuleDTO {
+ @NotBlank(message = "id is required")
+ private String id;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/SystemAlertController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/SystemAlertController.java
index ab22fd08..7dda1bf9 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/SystemAlertController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/SystemAlertController.java
@@ -17,6 +17,7 @@
package org.apache.rocketmq.studio.ops.alert;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -42,8 +43,8 @@ public class SystemAlertController {
}
@PostMapping("/acknowledge")
- public Result<SystemAlertVO> acknowledgeAlert(@RequestBody Map<String,
String> request) {
- return Result.ok(alertService.acknowledgeAlert(request.get("id")));
+ public Result<SystemAlertVO> acknowledgeAlert(@Valid @RequestBody
AcknowledgeSystemAlertDTO request) {
+ return Result.ok(alertService.acknowledgeAlert(request.getId()));
}
@PostMapping("/clear-acknowledged")
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/ToggleAlertRuleDTO.java
similarity index 64%
copy from
server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/ops/alert/ToggleAlertRuleDTO.java
index 571597b0..24935e96 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/ToggleAlertRuleDTO.java
@@ -14,12 +14,23 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.rocketmq.studio.ops.alert;
-package org.apache.rocketmq.studio.ops;
-
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class OpsVipChannelDTO {
- private boolean useVIPChannel;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ToggleAlertRuleDTO {
+ @NotBlank(message = "id is required")
+ private String id;
+
+ @NotNull(message = "enabled is required")
+ private Boolean enabled;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditCleanupDTO.java
similarity index 70%
copy from
server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
copy to
server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditCleanupDTO.java
index 571597b0..7c3fd411 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditCleanupDTO.java
@@ -14,12 +14,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package org.apache.rocketmq.studio.ops.audit;
-package org.apache.rocketmq.studio.ops;
-
+import jakarta.validation.constraints.Positive;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
import lombok.Data;
+import lombok.NoArgsConstructor;
@Data
-public class OpsVipChannelDTO {
- private boolean useVIPChannel;
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class AuditCleanupDTO {
+ @Positive(message = "beforeDays must be greater than 0")
+ private Integer beforeDays;
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
index 6f364f84..32d40492 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditController.java
@@ -18,6 +18,7 @@ package org.apache.rocketmq.studio.ops.audit;
import org.apache.rocketmq.studio.common.domain.PageResult;
import org.apache.rocketmq.studio.common.domain.Result;
+import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@@ -49,8 +50,8 @@ public class AuditController {
}
@PostMapping("/cleanup")
- public Result<Map<String, Integer>> cleanupLogs(@RequestBody Map<String,
Integer> request) {
- int beforeDays = request.getOrDefault("beforeDays", 30);
+ public Result<Map<String, Integer>> cleanupLogs(@Valid
@RequestBody(required = false) AuditCleanupDTO request) {
+ int beforeDays = request == null || request.getBeforeDays() == null ?
30 : request.getBeforeDays();
int deleted = auditService.cleanupLogs(beforeDays);
return Result.ok(Map.of("deleted", deleted));
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandlerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandlerTest.java
index 672d6d0b..c872f2d1 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandlerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/common/exception/GlobalExceptionHandlerTest.java
@@ -23,6 +23,7 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -56,6 +57,14 @@ class GlobalExceptionHandlerTest {
.andExpect(jsonPath("$.message").value("failure-400"));
}
+ @Test
+ void returnsBadRequestWhenRequiredRequestParamIsMissing() throws Exception
{
+ mockMvc.perform(get("/test/required-param"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("key is required"));
+ }
+
@RestController
static class FailingController {
@@ -63,5 +72,10 @@ class GlobalExceptionHandlerTest {
Result<Void> fail(@PathVariable int code) {
throw new BusinessException(code, "failure-" + code);
}
+
+ @GetMapping("/test/required-param")
+ Result<String> requiredParam(@RequestParam String key) {
+ return Result.ok(key);
+ }
}
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/OpsControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/OpsControllerTest.java
index e65409e5..d0c4c304 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/ops/OpsControllerTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/ops/OpsControllerTest.java
@@ -31,6 +31,7 @@ import java.util.Map;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
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;
@@ -80,6 +81,18 @@ class OpsControllerTest {
verify(opsService).updateNameServer(eq("10.0.0.1:9876"));
}
+ @Test
+ void updateNameSvrAddrShouldRejectMissingAddress() throws Exception {
+ mockMvc.perform(post("/api/ops/updateNameSvrAddr")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("namesrvAddr is
required"));
+
+ verifyNoInteractions(opsService);
+ }
+
@Test
void addNameSvrAddrShouldDelegateToService() throws Exception {
mockMvc.perform(post("/api/ops/addNameSvrAddr")
@@ -90,6 +103,18 @@ class OpsControllerTest {
verify(opsService).addNameServer(eq("10.0.0.2:9876"));
}
+ @Test
+ void addNameSvrAddrShouldRejectBlankAddress() throws Exception {
+ mockMvc.perform(post("/api/ops/addNameSvrAddr")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("namesrvAddr", " "))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("namesrvAddr is
required"));
+
+ verifyNoInteractions(opsService);
+ }
+
@Test
void updateVipChannelShouldDelegateToService() throws Exception {
mockMvc.perform(post("/api/ops/updateIsVIPChannel")
@@ -100,6 +125,18 @@ class OpsControllerTest {
verify(opsService).updateVipChannel(false);
}
+ @Test
+ void updateVipChannelShouldRejectMissingFlag() throws Exception {
+ mockMvc.perform(post("/api/ops/updateIsVIPChannel")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("useVIPChannel is
required"));
+
+ verifyNoInteractions(opsService);
+ }
+
@Test
void updateUseTlsShouldDelegateToService() throws Exception {
mockMvc.perform(post("/api/ops/updateUseTLS")
@@ -109,4 +146,16 @@ class OpsControllerTest {
verify(opsService).updateUseTLS(true);
}
+
+ @Test
+ void updateUseTlsShouldRejectMissingFlag() throws Exception {
+ mockMvc.perform(post("/api/ops/updateUseTLS")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("useTLS is required"));
+
+ verifyNoInteractions(opsService);
+ }
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleControllerTest.java
new file mode 100644
index 00000000..d8c3aeba
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertRuleControllerTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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 org.apache.rocketmq.studio.ops.alert;
+
+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.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+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(AlertRuleController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class AlertRuleControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockBean
+ private AlertService alertService;
+
+ @Test
+ void listRulesShouldReturnRules() throws Exception {
+ AlertRuleVO rule = AlertRuleVO.builder()
+ .id("rule-1")
+ .name("High Lag")
+ .metric("rocketmq_consumer_lag_messages")
+ .enabled(true)
+ .build();
+ when(alertService.listRules()).thenReturn(List.of(rule));
+
+ mockMvc.perform(get("/api/alert-rules"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data[0].id").value("rule-1"))
+ .andExpect(jsonPath("$.data[0].enabled").value(true));
+ }
+
+ @Test
+ void createRuleShouldReturnCreatedRule() throws Exception {
+ AlertRuleVO request = AlertRuleVO.builder()
+ .name("High Lag")
+ .metric("rocketmq_consumer_lag_messages")
+ .enabled(true)
+ .build();
+ AlertRuleVO created = AlertRuleVO.builder()
+ .id("rule-1")
+ .name("High Lag")
+ .metric("rocketmq_consumer_lag_messages")
+ .enabled(true)
+ .build();
+
when(alertService.createRule(any(AlertRuleVO.class))).thenReturn(created);
+
+ mockMvc.perform(post("/api/alert-rules/create")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(request)))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.id").value("rule-1"))
+ .andExpect(jsonPath("$.data.name").value("High Lag"));
+ }
+
+ @Test
+ void toggleRuleShouldPassValidatedRequest() throws Exception {
+ AlertRuleVO toggled = AlertRuleVO.builder()
+ .id("rule-1")
+ .name("High Lag")
+ .enabled(false)
+ .build();
+ when(alertService.toggleRule("rule-1", false)).thenReturn(toggled);
+
+ mockMvc.perform(post("/api/alert-rules/toggle")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
"rule-1", "enabled", false))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.id").value("rule-1"))
+ .andExpect(jsonPath("$.data.enabled").value(false));
+
+ verify(alertService).toggleRule(eq("rule-1"), eq(false));
+ }
+
+ @Test
+ void toggleRuleShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/toggle")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("enabled", true))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void toggleRuleShouldRejectMissingEnabled() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/toggle")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
"rule-1"))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("enabled is required"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void toggleRuleShouldRejectInvalidEnabledType() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/toggle")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
"rule-1", "enabled", "invalid"))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("Invalid request
body"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void deleteRuleShouldPassValidatedRequest() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
"rule-1"))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.message").value("success"));
+
+ verify(alertService).deleteRule("rule-1");
+ }
+
+ @Test
+ void deleteRuleShouldRejectBlankId() throws Exception {
+ mockMvc.perform(post("/api/alert-rules/delete")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
" "))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(alertService);
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/SystemAlertControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/SystemAlertControllerTest.java
new file mode 100644
index 00000000..ca655a62
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/SystemAlertControllerTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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 org.apache.rocketmq.studio.ops.alert;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.rocketmq.studio.common.domain.enums.AlertLevel;
+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.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+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(SystemAlertController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class SystemAlertControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockBean
+ private AlertService alertService;
+
+ @Test
+ void listAlertsShouldReturnSystemAlerts() throws Exception {
+ SystemAlertVO alert = SystemAlertVO.builder()
+ .id("alert-1")
+ .level(AlertLevel.error)
+ .title("Broker Down")
+ .acknowledged(false)
+ .build();
+ when(alertService.listAlerts("error")).thenReturn(List.of(alert));
+
+ mockMvc.perform(get("/api/system-alerts").param("level", "error"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data[0].id").value("alert-1"))
+ .andExpect(jsonPath("$.data[0].level").value("error"))
+ .andExpect(jsonPath("$.data[0].acknowledged").value(false));
+
+ verify(alertService).listAlerts("error");
+ }
+
+ @Test
+ void acknowledgeAlertShouldPassValidatedRequest() throws Exception {
+ SystemAlertVO acknowledged = SystemAlertVO.builder()
+ .id("alert-1")
+ .level(AlertLevel.warning)
+ .title("High Lag")
+ .acknowledged(true)
+ .build();
+
when(alertService.acknowledgeAlert("alert-1")).thenReturn(acknowledged);
+
+ mockMvc.perform(post("/api/system-alerts/acknowledge")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
"alert-1"))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.id").value("alert-1"))
+ .andExpect(jsonPath("$.data.acknowledged").value(true));
+
+ verify(alertService).acknowledgeAlert("alert-1");
+ }
+
+ @Test
+ void acknowledgeAlertShouldRejectBlankId() throws Exception {
+ mockMvc.perform(post("/api/system-alerts/acknowledge")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of("id",
" "))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void acknowledgeAlertShouldRejectMissingId() throws Exception {
+ mockMvc.perform(post("/api/system-alerts/acknowledge")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content(objectMapper.writeValueAsString(Map.of())))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("id is required"));
+
+ verifyNoInteractions(alertService);
+ }
+
+ @Test
+ void clearAcknowledgedShouldReturnClearedCount() throws Exception {
+ when(alertService.clearAcknowledged()).thenReturn(3);
+
+ mockMvc.perform(post("/api/system-alerts/clear-acknowledged"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.cleared").value(3));
+
+ verify(alertService).clearAcknowledged();
+ }
+}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditControllerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditControllerTest.java
new file mode 100644
index 00000000..b27dfe40
--- /dev/null
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditControllerTest.java
@@ -0,0 +1,160 @@
+/*
+ * 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 org.apache.rocketmq.studio.ops.audit;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.rocketmq.studio.common.domain.PageResult;
+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.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+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(AuditController.class)
+@AutoConfigureMockMvc(addFilters = false)
+class AuditControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @MockBean
+ private AuditService auditService;
+
+ @Test
+ void queryLogsShouldReturnPageResult() throws Exception {
+ AuditRecordVO record = AuditRecordVO.builder()
+ .operator("admin")
+ .operationType("DELETE")
+ .target("topic-a")
+ .result("SUCCESS")
+ .build();
+ when(auditService.queryLogs(eq(2), eq(10), eq("topic"), eq("DELETE"),
+ eq("2026-07-01"), eq("2026-07-24"), eq("SUCCESS")))
+ .thenReturn(PageResult.of(List.of(record), 1, 2, 10));
+
+ mockMvc.perform(get("/api/audit-logs")
+ .param("page", "2")
+ .param("pageSize", "10")
+ .param("search", "topic")
+ .param("operationType", "DELETE")
+ .param("startDate", "2026-07-01")
+ .param("endDate", "2026-07-24")
+ .param("result", "SUCCESS"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.items[0].operator").value("admin"))
+
.andExpect(jsonPath("$.data.items[0].operationType").value("DELETE"))
+ .andExpect(jsonPath("$.data.total").value(1));
+
+ verify(auditService).queryLogs(eq(2), eq(10), eq("topic"),
eq("DELETE"),
+ eq("2026-07-01"), eq("2026-07-24"), eq("SUCCESS"));
+ }
+
+ @Test
+ void cleanupLogsShouldUseProvidedRetention() throws Exception {
+ when(auditService.cleanupLogs(90)).thenReturn(7);
+
+ mockMvc.perform(post("/api/audit-logs/cleanup")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("beforeDays", 90))))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.code").value(200))
+ .andExpect(jsonPath("$.data.deleted").value(7));
+
+ verify(auditService).cleanupLogs(90);
+ }
+
+ @Test
+ void cleanupLogsShouldDefaultRetentionWhenBodyIsEmpty() throws Exception {
+ when(auditService.cleanupLogs(30)).thenReturn(3);
+
+ mockMvc.perform(post("/api/audit-logs/cleanup"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.deleted").value(3));
+
+ verify(auditService).cleanupLogs(30);
+ }
+
+ @Test
+ void cleanupLogsShouldDefaultRetentionWhenBeforeDaysIsMissing() throws
Exception {
+ when(auditService.cleanupLogs(30)).thenReturn(3);
+
+ mockMvc.perform(post("/api/audit-logs/cleanup")
+ .contentType(MediaType.APPLICATION_JSON)
+ .content("{}"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.deleted").value(3));
+
+ verify(auditService).cleanupLogs(30);
+ }
+
+ @Test
+ void cleanupLogsShouldRejectNonPositiveRetention() throws Exception {
+ mockMvc.perform(post("/api/audit-logs/cleanup")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("beforeDays", 0))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("beforeDays must be
greater than 0"));
+
+ verifyNoInteractions(auditService);
+ }
+
+ @Test
+ void cleanupLogsShouldRejectInvalidRetentionType() throws Exception {
+ mockMvc.perform(post("/api/audit-logs/cleanup")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("beforeDays", "invalid"))))
+ .andExpect(status().isBadRequest())
+ .andExpect(jsonPath("$.code").value(400))
+ .andExpect(jsonPath("$.message").value("Invalid request
body"));
+
+ verifyNoInteractions(auditService);
+ }
+
+ @Test
+ void queryLogsShouldUseDefaultPagination() throws Exception {
+ when(auditService.queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), isNull(), isNull()))
+ .thenReturn(PageResult.of(List.of(), 0, 1, 20));
+
+ mockMvc.perform(get("/api/audit-logs"))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.items").isArray())
+ .andExpect(jsonPath("$.data.page").value(1))
+ .andExpect(jsonPath("$.data.size").value(20));
+
+ verify(auditService).queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), isNull(), isNull());
+ }
+}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
b/web/src/pages/ops/__tests__/alerts.test.ts
similarity index 53%
copy from
server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
copy to web/src/pages/ops/__tests__/alerts.test.ts
index 571597b0..1da7b0e9 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
+++ b/web/src/pages/ops/__tests__/alerts.test.ts
@@ -15,11 +15,29 @@
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops;
+import { describe, expect, it } from 'vitest';
+import { attachThresholdUnit } from '../alertRulePayload';
-import lombok.Data;
+describe('attachThresholdUnit', () => {
+ it('derives the threshold unit from the selected metric', () => {
+ expect(attachThresholdUnit({ metric: 'Broker 离线', threshold: 1
})).toEqual({
+ metric: 'Broker 离线',
+ threshold: 1,
+ thresholdUnit: '个',
+ });
+ });
-@Data
-public class OpsVipChannelDTO {
- private boolean useVIPChannel;
-}
+ it('overwrites stale units when a metric changes', () => {
+ expect(
+ attachThresholdUnit({
+ metric: '消费堆积量',
+ threshold: 100,
+ thresholdUnit: '%',
+ }),
+ ).toEqual({
+ metric: '消费堆积量',
+ threshold: 100,
+ thresholdUnit: '条',
+ });
+ });
+});
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
b/web/src/pages/ops/alertRulePayload.ts
similarity index 67%
copy from
server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
copy to web/src/pages/ops/alertRulePayload.ts
index 571597b0..366fd182 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsVipChannelDTO.java
+++ b/web/src/pages/ops/alertRulePayload.ts
@@ -15,11 +15,19 @@
* limitations under the License.
*/
-package org.apache.rocketmq.studio.ops;
+export const thresholdUnits: Record<string, string> = {
+ 磁盘使用率: '%',
+ 消费堆积量: '条',
+ 'TPS 异常': 'TPS',
+ 'Broker 离线': '个',
+ 'Proxy 连接数': '个',
+};
-import lombok.Data;
-
-@Data
-public class OpsVipChannelDTO {
- private boolean useVIPChannel;
+export function attachThresholdUnit<T extends { metric: string }>(
+ values: T,
+): T & { thresholdUnit: string } {
+ return {
+ ...values,
+ thresholdUnit: thresholdUnits[values.metric] ?? '',
+ };
}
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index 3f7265ae..6bcbf8ba 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -43,6 +43,7 @@ import {
toggleAlertRule,
updateAlertRule,
} from '../../services/opsService';
+import { attachThresholdUnit } from './alertRulePayload';
const { TextArea } = Input;
@@ -56,14 +57,6 @@ const metricOptions = ['磁盘使用率', '消费堆积量', 'TPS 异常', 'Brok
const durationOptions = ['1分钟', '5分钟', '15分钟', '30分钟'];
-const thresholdUnits: Record<string, string> = {
- 磁盘使用率: '%',
- 消费堆积量: '条',
- 'TPS 异常': 'TPS',
- 'Broker 离线': '个',
- 'Proxy 连接数': '个',
-};
-
const AlertsPage = () => {
const { t } = useLang();
const [rules, setRules] = useState<AlertRule[]>([]);
@@ -222,18 +215,16 @@ const AlertsPage = () => {
const handleSubmit = async () => {
try {
const values = await form.validateFields();
+ const payload = attachThresholdUnit(values);
setSubmitting(true);
if (editingRule) {
- const updated = await updateAlertRule({ ...editingRule, ...values });
+ const updated = await updateAlertRule({ ...editingRule, ...payload });
setRules((previous) =>
previous.map((rule) => (rule.id === editingRule.id ? updated :
rule)),
);
message.success('告警规则已更新');
} else {
- const created = await createAlertRule({
- ...values,
- thresholdUnit: thresholdUnits[values.metric] ?? '',
- });
+ const created = await createAlertRule(payload);
setRules((previous) => [...previous, created]);
message.success(t('alerts.ruleCreated'));
}