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 799f459d1 feat(audit): add an operator filter to the audit feed (#7308)
799f459d1 is described below
commit 799f459d11e90fc4c6864973e0d9fd83924a68b0
Author: xdz <[email protected]>
AuthorDate: Mon Oct 26 17:19:46 2026 +0800
feat(audit): add an operator filter to the audit feed (#7308)
* fix(audit): align the operator filter with the current audit surfaces
* fix(audit): restore the missing-cluster coverage the operator filter
dropped
The operator filter arrived with three repository cases and one controller
case
deleted rather than updated, and AuditServiceTest still called the
pre-operator
signatures, so the branch did not compile. Restore all four cases against
the new
arity and add the equality assertion the operator filter needs — a LIKE
would also
match every "ops-zhang2" row and leak one operator's trail into another's
filter.
Also repair the findPage and applyFilters indentation the edit broke, and
document
the operator parameter plus the operators filter option in api-spec.md.
---
docs/api-spec.md | 6 +-
.../tool/handler/audit/AuditListToolHandler.java | 2 +-
.../rocketmq/studio/ops/audit/AuditController.java | 9 ++-
.../studio/ops/audit/AuditFilterOptionsVO.java | 1 +
.../rocketmq/studio/ops/audit/AuditRepository.java | 4 +-
.../rocketmq/studio/ops/audit/AuditService.java | 22 +++----
.../ops/audit/MybatisPlusAuditRepository.java | 23 ++++---
.../handler/audit/AuditListToolHandlerTest.java | 4 +-
.../studio/ops/audit/AuditControllerTest.java | 31 +++++----
.../studio/ops/audit/AuditServiceTest.java | 75 +++++++++++-----------
.../ops/audit/MybatisPlusAuditRepositoryTest.java | 50 ++++++++++-----
web/src/api/audit.ts | 1 +
web/src/api/ops.ts | 1 +
web/src/pages/ops/__tests__/AuditPage.test.tsx | 32 +++++++++
web/src/pages/ops/audit.tsx | 20 ++++++
web/src/services/opsService.ts | 2 +
16 files changed, 185 insertions(+), 98 deletions(-)
diff --git a/docs/api-spec.md b/docs/api-spec.md
index 9066bfba0..481f489b2 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -2458,7 +2458,7 @@ POST /api/system-alerts/clear-acknowledged
### 13.1 获取审计日志列表
```
-GET
/api/audit-logs?page={page}&pageSize={pageSize}&search={search}&operationType={type}&resourceType={resourceType}&target={target}&clusterId={clusterId}&clusterIdMissing={missing}&startDate={start}&endDate={end}&result={result}
+GET
/api/audit-logs?page={page}&pageSize={pageSize}&search={search}&operator={operator}&operationType={type}&resourceType={resourceType}&target={target}&clusterId={clusterId}&clusterIdMissing={missing}&startDate={start}&endDate={end}&result={result}
```
**Query Parameters:**
@@ -2468,6 +2468,7 @@ GET
/api/audit-logs?page={page}&pageSize={pageSize}&search={search}&operationTyp
| `page` | `number` | 否 | 页码,默认 1 |
| `pageSize` | `number` | 否 | 每页条数,默认 20 |
| `search` | `string` | 否 | 模糊搜索(匹配 operator / target / detail) |
+| `operator` | `string` | 否 | 操作人过滤,使用等值匹配而非模糊搜索;取值来自筛选项接口的 `operators` |
| `operationType` | `string` | 否 | 操作类型过滤 |
| `resourceType` | `string` | 否 | 资源类型过滤 |
| `target` | `string` | 否 | 操作对象精确过滤,使用等值匹配而非模糊搜索 |
@@ -2516,6 +2517,7 @@ GET /api/audit-logs/filter-options
| 字段 | 类型 | 说明 |
|------|------|------|
+| `operators` | `string[]` | 数据库中已存在的操作人原始值 |
| `operationTypes` | `string[]` | 数据库中已存在的操作类型原始值 |
| `resourceTypes` | `string[]` | 数据库中已存在的资源类型原始值 |
| `clusterIds` | `string[]` | 数据库中已存在的非空集群 ID |
@@ -2526,7 +2528,7 @@ GET /api/audit-logs/filter-options
### 13.3 导出审计日志
```
-GET
/api/audit-logs/export?search={search}&operationType={type}&resourceType={resourceType}&target={target}&clusterId={clusterId}&clusterIdMissing={missing}&startDate={start}&endDate={end}&result={result}
+GET
/api/audit-logs/export?search={search}&operator={operator}&operationType={type}&resourceType={resourceType}&target={target}&clusterId={clusterId}&clusterIdMissing={missing}&startDate={start}&endDate={end}&result={result}
```
查询参数与列表接口相同,但不包含 `page` 和 `pageSize`。接口返回全部匹配记录,不受当前表格分页影响。
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandler.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandler.java
index 71628541d..41794f36b 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandler.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandler.java
@@ -52,7 +52,7 @@ public class AuditListToolHandler implements
ToolHandler<AuditListInput, PageOut
public PageOutput<AuditItem> execute(AuditListInput input,
ToolExecutionContext context) {
PageResult<AuditRecordVO> result = auditService.queryLogs(
input.page().page(), input.page().pageSize(),
- input.search(), input.operationType(), input.resourceType(),
+ input.search(), null, input.operationType(),
input.resourceType(),
null, null, false, input.startDate(), input.endDate(),
input.result());
return PageOutput.from(result, AuditItem::from);
}
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 570015e61..08d964119 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
@@ -41,6 +41,7 @@ public class AuditController {
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int pageSize,
@RequestParam(required = false) String search,
+ @RequestParam(required = false) String operator,
@RequestParam(required = false) String operationType,
@RequestParam(required = false) String resourceType,
@RequestParam(required = false) String target,
@@ -49,7 +50,7 @@ public class AuditController {
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String result) {
- return Result.ok(auditService.queryLogs(page, pageSize, search,
operationType,
+ return Result.ok(auditService.queryLogs(page, pageSize, search,
operator, operationType,
resourceType, target, clusterId, clusterIdMissing, startDate,
endDate, result));
}
@@ -61,19 +62,21 @@ public class AuditController {
@GetMapping("/summary")
public Result<AuditSummaryVO> summarize(
@RequestParam(required = false) String search,
+ @RequestParam(required = false) String operator,
@RequestParam(required = false) String operationType,
@RequestParam(required = false) String resourceType,
@RequestParam(required = false) String clusterId,
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String result) {
- return Result.ok(auditService.summarize(search, operationType,
resourceType,
+ return Result.ok(auditService.summarize(search, operator,
operationType, resourceType,
clusterId, startDate, endDate, result));
}
@GetMapping("/export")
public Result<String> exportLogs(
@RequestParam(required = false) String search,
+ @RequestParam(required = false) String operator,
@RequestParam(required = false) String operationType,
@RequestParam(required = false) String resourceType,
@RequestParam(required = false) String target,
@@ -82,7 +85,7 @@ public class AuditController {
@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate,
@RequestParam(required = false) String result) {
- return Result.ok(auditService.exportLogs(search, operationType,
resourceType,
+ return Result.ok(auditService.exportLogs(search, operator,
operationType, resourceType,
target, clusterId, clusterIdMissing, startDate, endDate,
result));
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditFilterOptionsVO.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditFilterOptionsVO.java
index 43a6b951b..bbf9d7091 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditFilterOptionsVO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditFilterOptionsVO.java
@@ -28,6 +28,7 @@ import java.util.List;
@NoArgsConstructor
@AllArgsConstructor
public class AuditFilterOptionsVO {
+ private List<String> operators;
private List<String> operationTypes;
private List<String> resourceTypes;
private List<String> clusterIds;
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditRepository.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditRepository.java
index 96a6773a3..0f9aeddfb 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditRepository.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditRepository.java
@@ -20,7 +20,7 @@ import java.time.LocalDateTime;
import org.apache.rocketmq.studio.common.domain.PageResult;
public interface AuditRepository {
- PageResult<AuditRecordVO> findPage(String search, String operationType,
+ PageResult<AuditRecordVO> findPage(String search, String operator, String
operationType,
String resourceType, String target,
String clusterId,
boolean clusterIdMissing,
LocalDateTime startDate, LocalDateTime
endDate,
@@ -28,7 +28,7 @@ public interface AuditRepository {
AuditFilterOptionsVO findFilterOptions();
- AuditSummaryVO summarize(String search, String operationType, String
resourceType,
+ AuditSummaryVO summarize(String search, String operator, String
operationType, String resourceType,
String clusterId, LocalDateTime startDate,
LocalDateTime endDate,
String result);
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
index 390ec6722..a4d2d345d 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/AuditService.java
@@ -49,16 +49,16 @@ public class AuditService {
private final AuditRepository auditRepository;
- public PageResult<AuditRecordVO> queryLogs(int page, int pageSize, String
search,
+ public PageResult<AuditRecordVO> queryLogs(int page, int pageSize, String
search, String operator,
String operationType, String
resourceType,
String target, String clusterId,
boolean clusterIdMissing,
String startDate,
String endDate, String result) {
validatePagination(page, pageSize);
- log.info("Querying audit logs, page={}, pageSize={}, search={},
operationType={}, result={}",
- page, pageSize, search, operationType, result);
+ log.info("Querying audit logs, page={}, pageSize={}, search={},
operator={}, operationType={}, result={}",
+ page, pageSize, search, operator, operationType, result);
- PageResult<AuditRecordVO> stored = findPage(search, operationType,
resourceType, target,
+ PageResult<AuditRecordVO> stored = findPage(search, operator,
operationType, resourceType, target,
clusterId, clusterIdMissing, startDate, endDate, result, page,
pageSize);
if (stored == null || stored.getItems() == null) {
return stored;
@@ -77,11 +77,11 @@ public class AuditService {
return auditRepository.findFilterOptions();
}
- public AuditSummaryVO summarize(String search, String operationType,
String resourceType,
+ public AuditSummaryVO summarize(String search, String operator, String
operationType, String resourceType,
String clusterId, String startDate, String
endDate, String result) {
DateRange range = parseDateRange(startDate, endDate);
String normalizedSearch = normalizeSearch(search);
- AuditSummaryVO summary = auditRepository.summarize(normalizedSearch,
operationType,
+ AuditSummaryVO summary = auditRepository.summarize(normalizedSearch,
operator, operationType,
resourceType, clusterId, range.start(), range.end(), result);
// latestAt rides the same wire as the page rows: zone-less
server-local in storage,
// UTC for every JSON consumer, while the audit CSV keeps naming the
server offset.
@@ -92,11 +92,11 @@ public class AuditService {
return summary;
}
- public String exportLogs(String search, String operationType, String
resourceType,
+ public String exportLogs(String search, String operator, String
operationType, String resourceType,
String target, String clusterId, boolean
clusterIdMissing, String startDate,
String endDate, String result) {
PageResult<AuditRecordVO> page = findPage(
- search, operationType, resourceType, target, clusterId,
clusterIdMissing,
+ search, operator, operationType, resourceType, target,
clusterId, clusterIdMissing,
startDate, endDate, result, 1, MAX_EXPORT_RECORDS);
if (page.getTotal() > MAX_EXPORT_RECORDS) {
throw new BusinessException(400,
@@ -202,15 +202,15 @@ public class AuditService {
}
}
- private PageResult<AuditRecordVO> findPage(String search, String
operationType,
+ private PageResult<AuditRecordVO> findPage(String search, String operator,
String operationType,
String resourceType, String
target, String clusterId,
boolean clusterIdMissing,
String startDate, String
endDate,
String result, int page, int
pageSize) {
DateRange range = parseDateRange(startDate, endDate);
String normalizedSearch = normalizeSearch(search);
- return auditRepository.findPage(normalizedSearch, operationType,
resourceType, target, clusterId,
- clusterIdMissing,
+ return auditRepository.findPage(normalizedSearch, operator,
operationType, resourceType, target,
+ clusterId, clusterIdMissing,
range.start(), range.end(), result, page, pageSize);
}
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
index 8caf98497..787ea5a37 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepository.java
@@ -53,14 +53,14 @@ public class MybatisPlusAuditRepository implements
AuditRepository {
private volatile CachedFilterOptions cachedFilterOptions;
@Override
- public PageResult<AuditRecordVO> findPage(String search, String
operationType,
+ public PageResult<AuditRecordVO> findPage(String search, String operator,
String operationType,
String resourceType, String
target, String clusterId,
boolean clusterIdMissing,
LocalDateTime startDate,
LocalDateTime endDate,
String result, int page, int
pageSize) {
QueryWrapper<RmqOperationAudit> query = new QueryWrapper<>();
- applyFilters(query, search, operationType, resourceType, target,
clusterId, clusterIdMissing,
- startDate, endDate, result);
+ applyFilters(query, search, operator, operationType, resourceType,
target, clusterId,
+ clusterIdMissing, startDate, endDate, result);
query.orderByDesc("gmt_create", "id");
Page<RmqOperationAudit> resultPage = auditMapper.selectPage(
new Page<>(page, pageSize), query);
@@ -91,9 +91,10 @@ public class MybatisPlusAuditRepository implements
AuditRepository {
private AuditFilterOptionsVO loadFilterOptions() {
List<Map<String, Object>> values = auditMapper.selectMaps(
new QueryWrapper<RmqOperationAudit>()
- .select("operation", "resource_type", "cluster_id",
"result")
- .groupBy("operation", "resource_type", "cluster_id",
"result"));
+ .select("operation", "resource_type", "cluster_id",
"result", "operator")
+ .groupBy("operation", "resource_type", "cluster_id",
"result", "operator"));
return AuditFilterOptionsVO.builder()
+ .operators(findDistinctValues(values, "operator"))
.operationTypes(findDistinctValues(values, "operation"))
.resourceTypes(findDistinctValues(values, "resource_type"))
.clusterIds(findDistinctValues(values, "cluster_id"))
@@ -102,11 +103,12 @@ public class MybatisPlusAuditRepository implements
AuditRepository {
}
@Override
- public AuditSummaryVO summarize(String search, String operationType,
String resourceType,
- String clusterId, LocalDateTime startDate,
LocalDateTime endDate,
+ public AuditSummaryVO summarize(String search, String operator, String
operationType,
+ String resourceType, String clusterId,
+ LocalDateTime startDate, LocalDateTime
endDate,
String result) {
Consumer<QueryWrapper<RmqOperationAudit>> filters = query ->
applyFilters(query, search,
- operationType, resourceType, null, clusterId, false,
startDate, endDate, result);
+ operator, operationType, resourceType, null, clusterId, false,
startDate, endDate, result);
// One GROUP BY result query computes total / SUCCESS / FAILED /
PARTIAL in a single
// round trip instead of four separate COUNT(*) statements. Note that
when the caller
@@ -192,13 +194,14 @@ public class MybatisPlusAuditRepository implements
AuditRepository {
* hotspot queries so a card can never disagree with the rows it describes.
*/
private void applyFilters(QueryWrapper<RmqOperationAudit> query, String
search,
- String operationType, String resourceType,
String target,
- String clusterId, boolean clusterIdMissing,
+ String operator, String operationType, String
resourceType,
+ String target, String clusterId, boolean
clusterIdMissing,
LocalDateTime startDate, LocalDateTime endDate,
String result) {
query.and(StringUtils.hasText(search), w -> w
.like("operator", search)
.or().like("resource_name", search)
.or().like("detail", search))
+ .eq(StringUtils.hasText(operator), "operator", operator)
.eq(StringUtils.hasText(operationType), "operation",
operationType)
.eq(StringUtils.hasText(resourceType), "resource_type",
resourceType)
.eq(StringUtils.hasText(target), "resource_name", target)
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandlerTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandlerTest.java
index ef6b94051..2717e67a0 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandlerTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/ai/tool/handler/audit/AuditListToolHandlerTest.java
@@ -61,7 +61,7 @@ class AuditListToolHandlerTest {
.result("SUCCESS")
.build();
record.setId(42L);
- when(auditService.queryLogs(eq(1), eq(20), eq("topic"), eq("DELETE"),
eq("TOPIC"),
+ when(auditService.queryLogs(eq(1), eq(20), eq("topic"), isNull(),
eq("DELETE"), eq("TOPIC"),
isNull(), isNull(), eq(false), eq("2026-09-01"),
eq("2026-09-14"), eq("SUCCESS")))
.thenReturn(PageResult.of(List.of(record), 1, 1, 20));
@@ -76,7 +76,7 @@ class AuditListToolHandlerTest {
assertThat(item.operationType()).isEqualTo("DELETE_TOPIC");
// Rows written by another Instance stay visible: cluster_id carries
the attribution.
assertThat(item.clusterId()).isEqualTo("instance-b");
- verify(auditService).queryLogs(eq(1), eq(20), eq("topic"),
eq("DELETE"), eq("TOPIC"),
+ verify(auditService).queryLogs(eq(1), eq(20), eq("topic"), isNull(),
eq("DELETE"), eq("TOPIC"),
isNull(), isNull(), eq(false), eq("2026-09-01"),
eq("2026-09-14"), eq("SUCCESS"));
}
}
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
index c707013c1..2282242e5 100644
---
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
@@ -67,15 +67,15 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
.target("topic-a")
.result("SUCCESS")
.build();
- when(auditService.queryLogs(eq(2), eq(10), eq("topic"), eq("DELETE"),
- eq("TOPIC"), eq("topic-a"), eq("prod-cn"), eq(false),
eq("2026-07-01"),
- eq("2026-07-24"), eq("SUCCESS")))
+ when(auditService.queryLogs(eq(2), eq(10), eq("topic"), eq("admin"),
eq("DELETE"),
+ eq("TOPIC"), eq("topic-a"), eq("prod-cn"), eq(false),
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("operator", "admin")
.param("operationType", "DELETE")
.param("resourceType", "TOPIC")
.param("target", "topic-a")
@@ -89,9 +89,8 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
.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("TOPIC"), eq("topic-a"), eq("prod-cn"), eq(false),
eq("2026-07-01"),
- eq("2026-07-24"), eq("SUCCESS"));
+ verify(auditService).queryLogs(eq(2), eq(10), eq("topic"),
eq("admin"), eq("DELETE"),
+ eq("TOPIC"), eq("topic-a"), eq("prod-cn"), eq(false),
eq("2026-07-01"), eq("2026-07-24"), eq("SUCCESS"));
}
@Test
@@ -170,7 +169,7 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
@Test
void queryLogsShouldUseDefaultPagination() throws Exception {
- when(auditService.queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), isNull(),
+ when(auditService.queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), isNull(), isNull(),
isNull(), eq(false), isNull(), isNull(), isNull()))
.thenReturn(PageResult.of(List.of(), 0, 1, 20));
@@ -180,13 +179,13 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
.andExpect(jsonPath("$.data.page").value(1))
.andExpect(jsonPath("$.data.size").value(20));
- verify(auditService).queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), isNull(),
+ verify(auditService).queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), isNull(), isNull(),
isNull(), eq(false), isNull(), isNull(), isNull());
}
@Test
void queryLogsShouldForwardAnExplicitMissingClusterScopeTest() throws
Exception {
- when(auditService.queryLogs(eq(1), eq(20), isNull(), isNull(),
eq("SETTINGS"),
+ when(auditService.queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), eq("SETTINGS"),
eq("general"), isNull(), eq(true), isNull(), isNull(),
isNull()))
.thenReturn(PageResult.of(List.of(), 0, 1, 20));
@@ -196,19 +195,20 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
.param("clusterIdMissing", "true"))
.andExpect(status().isOk());
- verify(auditService).queryLogs(eq(1), eq(20), isNull(), isNull(),
eq("SETTINGS"),
+ verify(auditService).queryLogs(eq(1), eq(20), isNull(), isNull(),
isNull(), eq("SETTINGS"),
eq("general"), isNull(), eq(true), isNull(), isNull(),
isNull());
}
@Test
void exportLogsShouldForwardFilters() throws Exception {
String csv =
"\uFEFFtimestamp,operator\r\n\"2026-08-01T09:30\",\"admin\"\r\n";
- when(auditService.exportLogs(eq("topic"), eq("DELETE"), eq("TOPIC"),
eq("topic-a"),
+ when(auditService.exportLogs(eq("topic"), eq("admin"), eq("DELETE"),
eq("TOPIC"), eq("topic-a"),
eq("prod-cn"), eq(false), eq("2026-08-01"), eq("2026-08-02"),
eq("SUCCESS"))).thenReturn(csv);
mockMvc.perform(get("/api/audit-logs/export")
.param("search", "topic")
+ .param("operator", "admin")
.param("operationType", "DELETE")
.param("resourceType", "TOPIC")
.param("target", "topic-a")
@@ -220,13 +220,14 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
.andExpect(jsonPath("$.code").value(200))
.andExpect(jsonPath("$.data").value(csv));
- verify(auditService).exportLogs(eq("topic"), eq("DELETE"),
eq("TOPIC"), eq("topic-a"),
+ verify(auditService).exportLogs(eq("topic"), eq("admin"),
eq("DELETE"), eq("TOPIC"), eq("topic-a"),
eq("prod-cn"), eq(false), eq("2026-08-01"), eq("2026-08-02"),
eq("SUCCESS"));
}
@Test
void getFilterOptionsShouldReturnPersistedFacets() throws Exception {
AuditFilterOptionsVO options = AuditFilterOptionsVO.builder()
+ .operators(List.of("admin", "ops-user"))
.operationTypes(List.of("CREATE_TOPIC", "DELETE_TOPIC"))
.resourceTypes(List.of("TOPIC"))
.clusterIds(List.of("prod-cn"))
@@ -236,6 +237,7 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
mockMvc.perform(get("/api/audit-logs/filter-options"))
.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data.operators[0]").value("admin"))
.andExpect(jsonPath("$.data.operationTypes[0]").value("CREATE_TOPIC"))
.andExpect(jsonPath("$.data.resourceTypes[0]").value("TOPIC"))
.andExpect(jsonPath("$.data.clusterIds[0]").value("prod-cn"))
@@ -252,11 +254,12 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
.byOperation(List.of(new AuditSummaryBucketVO("DELETE_TOPIC",
5)))
.byResourceType(List.of(new AuditSummaryBucketVO("TOPIC", 8)))
.build();
- when(auditService.summarize(eq("topic"), eq("DELETE_TOPIC"),
eq("TOPIC"), eq("prod-cn"),
+ when(auditService.summarize(eq("topic"), eq("admin"),
eq("DELETE_TOPIC"), eq("TOPIC"), eq("prod-cn"),
eq("2026-08-01"), eq("2026-08-12"),
eq("SUCCESS"))).thenReturn(summary);
mockMvc.perform(get("/api/audit-logs/summary")
.param("search", "topic")
+ .param("operator", "admin")
.param("operationType", "DELETE_TOPIC")
.param("resourceType", "TOPIC")
.param("clusterId", "prod-cn")
@@ -269,7 +272,7 @@ class AuditControllerTest extends WebMvcAuthTestSupport {
.andExpect(jsonPath("$.data.uniqueOperators").value(3))
.andExpect(jsonPath("$.data.byOperation[0].name").value("DELETE_TOPIC"));
- verify(auditService).summarize(eq("topic"), eq("DELETE_TOPIC"),
eq("TOPIC"), eq("prod-cn"),
+ verify(auditService).summarize(eq("topic"), eq("admin"),
eq("DELETE_TOPIC"), eq("TOPIC"), eq("prod-cn"),
eq("2026-08-01"), eq("2026-08-12"), eq("SUCCESS"));
}
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
index c1f1e9c1f..64c5bd063 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/AuditServiceTest.java
@@ -91,18 +91,17 @@ class AuditServiceTest {
@Test
void queryLogsDelegatesPaginationAndFiltersToRepository() {
AuditRecordVO record =
AuditRecordVO.builder().operationType("CREATE").build();
- when(auditRepository.findPage(eq("topic-a"), eq("CREATE"),
eq("TOPIC"), eq("orders"),
- eq("prod-cn"), eq(false), isNull(), isNull(), eq("SUCCESS"),
eq(2), eq(20)))
+ when(auditRepository.findPage(eq("topic-a"), eq("admin"),
eq("CREATE"), eq("TOPIC"), eq("prod-cn"),
+ isNull(), eq(false), isNull(), isNull(), eq("SUCCESS"), eq(2),
eq(20)))
.thenReturn(PageResult.of(List.of(record), 21, 2, 20));
PageResult<AuditRecordVO> result = auditService.queryLogs(
- 2, 20, "topic-a", "CREATE", "TOPIC", "orders", "prod-cn",
false,
- null, null, "SUCCESS");
+ 2, 20, "topic-a", "admin", "CREATE", "TOPIC", "prod-cn", null,
false, null, null, "SUCCESS");
assertThat(result.getItems()).containsExactly(record);
assertThat(result.getTotal()).isEqualTo(21);
- verify(auditRepository).findPage(eq("topic-a"), eq("CREATE"),
eq("TOPIC"), eq("orders"),
- eq("prod-cn"), eq(false), isNull(), isNull(), eq("SUCCESS"),
eq(2), eq(20));
+ verify(auditRepository).findPage(eq("topic-a"), eq("admin"),
eq("CREATE"), eq("TOPIC"), eq("prod-cn"),
+ isNull(), eq(false), isNull(), isNull(), eq("SUCCESS"), eq(2),
eq(20));
}
@Test
@@ -119,11 +118,11 @@ class AuditServiceTest {
.timestamp(LocalDateTime.of(2026, 8, 1, 10, 0, 0))
.build();
when(auditRepository.findPage(isNull(), isNull(), isNull(),
isNull(), isNull(),
- eq(false), isNull(), isNull(), isNull(), eq(1), eq(20)))
+ isNull(), eq(false), isNull(), isNull(), isNull(), eq(1),
eq(20)))
.thenReturn(PageResult.of(List.of(stored), 1, 1, 20));
PageResult<AuditRecordVO> result = auditService.queryLogs(
- 1, 20, null, null, null, null, null, false, null, null,
null);
+ 1, 20, null, null, null, null, null, null, false, null,
null, null);
assertThat(result.getItems().get(0).getTimestamp())
.isEqualTo(LocalDateTime.of(2026, 8, 1, 2, 0, 0));
@@ -146,9 +145,9 @@ class AuditServiceTest {
.latestAt(LocalDateTime.of(2026, 8, 1, 10, 0, 0))
.build();
when(auditRepository.summarize(isNull(), isNull(), isNull(),
isNull(),
- isNull(), isNull(), isNull())).thenReturn(stored);
+ isNull(), isNull(), isNull(),
isNull())).thenReturn(stored);
- AuditSummaryVO result = auditService.summarize(null, null, null,
null, null, null, null);
+ AuditSummaryVO result = auditService.summarize(null, null, null,
null, null, null, null, null);
assertThat(result.getLatestAt()).isEqualTo(LocalDateTime.of(2026,
8, 1, 2, 0, 0));
} finally {
@@ -159,28 +158,28 @@ class AuditServiceTest {
@Test
void queryLogsParsesDateRangeBeforeDelegating() {
when(auditRepository.findPage(isNull(), isNull(), isNull(), isNull(),
- isNull(), eq(false), any(LocalDateTime.class),
any(LocalDateTime.class),
+ isNull(), isNull(), eq(false), any(LocalDateTime.class),
any(LocalDateTime.class),
isNull(), eq(1), eq(10)))
.thenReturn(PageResult.empty(1, 10));
- auditService.queryLogs(1, 10, null, null, null, null, null, false,
+ auditService.queryLogs(1, 10, null, null, null, null, null, null,
false,
"2026-08-01", "2026-08-02", null);
ArgumentCaptor<LocalDateTime> start =
ArgumentCaptor.forClass(LocalDateTime.class);
ArgumentCaptor<LocalDateTime> end =
ArgumentCaptor.forClass(LocalDateTime.class);
verify(auditRepository).findPage(isNull(), isNull(), isNull(),
isNull(),
- isNull(), eq(false), start.capture(), end.capture(), isNull(),
eq(1), eq(10));
+ isNull(), isNull(), eq(false), start.capture(), end.capture(),
isNull(), eq(1), eq(10));
assertThat(start.getValue()).isEqualTo(LocalDateTime.of(2026, 8, 1, 0,
0));
assertThat(end.getValue()).isEqualTo(LocalDateTime.of(2026, 8, 2, 23,
59, 59, 999_999_999));
}
@Test
void queryLogsRejectsInvalidPageBounds() {
- assertThatThrownBy(() -> auditService.queryLogs(0, 10, null, null,
null, null,
+ assertThatThrownBy(() -> auditService.queryLogs(0, 10, null, null,
null, null, null,
null, false, null, null, null))
.isInstanceOf(BusinessException.class)
.hasMessage("page must be greater than 0");
- assertThatThrownBy(() -> auditService.queryLogs(1, 101, null, null,
null, null,
+ assertThatThrownBy(() -> auditService.queryLogs(1, 101, null, null,
null, null, null,
null, false, null, null, null))
.isInstanceOf(BusinessException.class)
.hasMessage("pageSize must be between 1 and 100");
@@ -188,7 +187,7 @@ class AuditServiceTest {
@Test
void queryLogsRejectsInvalidDateRange() {
- assertThatThrownBy(() -> auditService.queryLogs(1, 10, null, null,
null, null,
+ assertThatThrownBy(() -> auditService.queryLogs(1, 10, null, null,
null, null, null,
null, false, "2026-08-02", "2026-08-01", null))
.isInstanceOf(BusinessException.class)
.hasMessage("startDate must not be after endDate");
@@ -207,13 +206,12 @@ class AuditServiceTest {
.result("FAILED")
.errorMessage("=denied")
.build();
- when(auditRepository.findPage(eq("topic"), eq("DELETE"), eq("TOPIC"),
eq("topic,a"),
- eq("prod-cn"), eq(false), any(LocalDateTime.class),
any(LocalDateTime.class),
- eq("FAILED"), eq(1), eq(10_000)))
+ when(auditRepository.findPage(eq("topic"), eq("admin"), eq("DELETE"),
eq("TOPIC"), eq("prod-cn"),
+ isNull(), eq(false), any(LocalDateTime.class),
any(LocalDateTime.class), eq("FAILED"), eq(1), eq(10_000)))
.thenReturn(PageResult.of(List.of(record), 1, 1, 10_000));
- String csv = auditService.exportLogs("topic", "DELETE", "TOPIC",
"topic,a", "prod-cn", false,
- "2026-08-01", "2026-08-02", "FAILED");
+ String csv = auditService.exportLogs("topic", "admin", "DELETE",
"TOPIC", "prod-cn",
+ null, false, "2026-08-01", "2026-08-02", "FAILED");
assertThat(csv).contains("resourceType,target,clusterId,detail,result,errorMessage")
.contains("\"'=cmd\",\"DELETE\",\"TOPIC\",\"topic,a\",\"prod-cn\"")
@@ -230,7 +228,7 @@ class AuditServiceTest {
.target("topic-a")
.result("SUCCESS")
.build();
- when(auditRepository.findPage(isNull(), isNull(), isNull(), isNull(),
isNull(), eq(false),
+ when(auditRepository.findPage(isNull(), isNull(), isNull(), isNull(),
isNull(), isNull(), eq(false),
any(LocalDateTime.class), any(LocalDateTime.class), isNull(),
eq(1), eq(10_000)))
.thenReturn(PageResult.of(List.of(record), 1, 1, 10_000));
@@ -239,12 +237,12 @@ class AuditServiceTest {
// The stored base is server-local and stays unconverted; only the
column name
// tells the consumer which zone the values are in.
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai"));
- assertThat(auditService.exportLogs(null, null, null, null, null,
false, "2026-08-01", "2026-08-02", null))
+ assertThat(auditService.exportLogs(null, null, null, null, null,
null, false, "2026-08-01", "2026-08-02", null))
.startsWith("\uFEFFtimestamp(UTC+08:00),operator,")
.contains("\"2026-08-01T09:30\"");
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
- assertThat(auditService.exportLogs(null, null, null, null, null,
false, "2026-08-01", "2026-08-02", null))
+ assertThat(auditService.exportLogs(null, null, null, null, null,
null, false, "2026-08-01", "2026-08-02", null))
.startsWith("\uFEFFtimestamp(UTC),operator,");
} finally {
TimeZone.setDefault(originalZone);
@@ -254,11 +252,11 @@ class AuditServiceTest {
@Test
void exportLogsRejectsResultsBeyondBound() {
when(auditRepository.findPage(isNull(), isNull(), isNull(), isNull(),
isNull(),
- eq(false), isNull(), isNull(), isNull(), eq(1), eq(10_000)))
+ isNull(), eq(false), isNull(), isNull(), isNull(), eq(1),
eq(10_000)))
.thenReturn(PageResult.of(List.of(), 10_001, 1, 10_000));
assertThatThrownBy(() -> auditService.exportLogs(
- null, null, null, null, null, false, null, null, null))
+ null, null, null, null, null, null, false, null, null, null))
.isInstanceOf(BusinessException.class)
.hasMessage("Audit log export exceeds the maximum of 10000
records; narrow the filters");
}
@@ -266,6 +264,7 @@ class AuditServiceTest {
@Test
void getFilterOptionsReturnsRepositoryValues() {
AuditFilterOptionsVO options = AuditFilterOptionsVO.builder()
+ .operators(List.of("admin"))
.operationTypes(List.of("CREATE_TOPIC"))
.resourceTypes(List.of("TOPIC"))
.clusterIds(List.of("prod-cn"))
@@ -280,16 +279,16 @@ class AuditServiceTest {
@Test
void summarizeParsesAndForwardsTheSharedFilterRangeTest() {
AuditSummaryVO summary =
AuditSummaryVO.builder().total(12).successful(10).failed(2).build();
- when(auditRepository.summarize(eq("topic"), eq("DELETE_TOPIC"),
eq("TOPIC"), eq("prod-cn"),
+ when(auditRepository.summarize(eq("topic"), eq("admin"),
eq("DELETE_TOPIC"), eq("TOPIC"), eq("prod-cn"),
any(LocalDateTime.class), any(LocalDateTime.class),
eq("FAILED"))).thenReturn(summary);
- AuditSummaryVO actual = auditService.summarize("topic",
"DELETE_TOPIC", "TOPIC", "prod-cn",
+ AuditSummaryVO actual = auditService.summarize("topic", "admin",
"DELETE_TOPIC", "TOPIC", "prod-cn",
"2026-08-01", "2026-08-02", "FAILED");
assertThat(actual).isSameAs(summary);
ArgumentCaptor<LocalDateTime> start =
ArgumentCaptor.forClass(LocalDateTime.class);
ArgumentCaptor<LocalDateTime> end =
ArgumentCaptor.forClass(LocalDateTime.class);
- verify(auditRepository).summarize(eq("topic"), eq("DELETE_TOPIC"),
eq("TOPIC"), eq("prod-cn"),
+ verify(auditRepository).summarize(eq("topic"), eq("admin"),
eq("DELETE_TOPIC"), eq("TOPIC"), eq("prod-cn"),
start.capture(), end.capture(), eq("FAILED"));
assertThat(start.getValue()).isEqualTo(LocalDateTime.of(2026, 8, 1, 0,
0));
assertThat(end.getValue()).isEqualTo(LocalDateTime.of(2026, 8, 2, 23,
59, 59, 999_999_999));
@@ -321,45 +320,45 @@ class AuditServiceTest {
@Test
void queryLogsShouldTrimSearchTermBeforeDelegating() {
- auditService.queryLogs(1, 10, " ops ", null, null, null, null, false,
+ auditService.queryLogs(1, 10, " ops ", null, null, null, null, null,
false,
null, null, null);
ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
- verify(auditRepository).findPage(search.capture(), isNull(), isNull(),
isNull(),
+ verify(auditRepository).findPage(search.capture(), isNull(), isNull(),
isNull(), isNull(),
isNull(), eq(false), isNull(), isNull(), isNull(), eq(1),
eq(10));
assertThat(search.getValue()).isEqualTo("ops");
}
@Test
void queryLogsShouldTreatWhitespaceOnlySearchAsAbsent() {
- auditService.queryLogs(1, 10, " ", null, null, null, null, false,
+ auditService.queryLogs(1, 10, " ", null, null, null, null, null,
false,
null, null, null);
ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
- verify(auditRepository).findPage(search.capture(), isNull(), isNull(),
isNull(),
+ verify(auditRepository).findPage(search.capture(), isNull(), isNull(),
isNull(), isNull(),
isNull(), eq(false), isNull(), isNull(), isNull(), eq(1),
eq(10));
assertThat(search.getValue()).isNull();
}
@Test
void exportLogsShouldTrimSearchTermBeforeDelegating() {
- when(auditRepository.findPage(any(), any(), any(), any(), any(),
anyBoolean(),
+ when(auditRepository.findPage(any(), any(), any(), any(), any(),
any(), anyBoolean(),
any(), any(), any(), anyInt(),
anyInt())).thenReturn(PageResult.empty(1, 10));
- auditService.exportLogs(" 50% off ", null, null, null, null, false,
null, null, null);
+ auditService.exportLogs(" 50% off ", null, null, null, null, null,
false, null, null, null);
ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
- verify(auditRepository).findPage(search.capture(), any(), any(),
any(), any(), anyBoolean(),
+ verify(auditRepository).findPage(search.capture(), any(), any(),
any(), any(), any(), anyBoolean(),
any(), any(), any(), anyInt(), anyInt());
assertThat(search.getValue()).isEqualTo("50% off");
}
@Test
void summarizeShouldTrimSearchTermBeforeDelegating() {
- auditService.summarize(" ops ", null, null, null, null, null, null);
+ auditService.summarize(" ops ", null, null, null, null, null, null,
null);
ArgumentCaptor<String> search = ArgumentCaptor.forClass(String.class);
- verify(auditRepository).summarize(search.capture(), isNull(),
isNull(), isNull(),
+ verify(auditRepository).summarize(search.capture(), isNull(),
isNull(), isNull(), isNull(),
isNull(), isNull(), isNull());
assertThat(search.getValue()).isEqualTo("ops");
}
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
index 41b0b880f..12187b675 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/audit/MybatisPlusAuditRepositoryTest.java
@@ -68,8 +68,7 @@ class MybatisPlusAuditRepositoryTest {
when(auditMapper.selectPage(any(IPage.class),
any(Wrapper.class))).thenReturn(mapperPage);
PageResult<AuditRecordVO> result = repository.findPage(
- "orders", "DELETE_TOPIC", "TOPIC", "orders", "prod-cn", false,
- null, null, "FAILED", 2, 25);
+ "orders", "admin", "DELETE_TOPIC", "TOPIC", null, "prod-cn",
false, null, null, "FAILED", 2, 25);
ArgumentCaptor<IPage<RmqOperationAudit>> pageCaptor =
ArgumentCaptor.forClass(IPage.class);
ArgumentCaptor<Wrapper<RmqOperationAudit>> queryCaptor =
ArgumentCaptor.forClass(Wrapper.class);
@@ -83,8 +82,8 @@ class MybatisPlusAuditRepositoryTest {
assertThat(record.getClusterId()).isEqualTo("prod-cn");
assertThat(record.getErrorMessage()).isEqualTo("denied");
assertThat(queryCaptor.getValue().getSqlSegment())
- .contains("operation", "resource_type", "resource_name =",
"cluster_id",
- "result", "gmt_create", "id");
+ .contains("operator", "operation", "resource_type",
"cluster_id", "result",
+ "gmt_create", "id");
}
@Test
@@ -92,7 +91,7 @@ class MybatisPlusAuditRepositoryTest {
when(auditMapper.selectPage(any(IPage.class), any(Wrapper.class)))
.thenReturn(new Page<RmqOperationAudit>(1,
20).setRecords(List.of()).setTotal(0));
- repository.findPage(null, null, "TOPIC", "orders", "prod-cn", false,
+ repository.findPage(null, null, null, "TOPIC", "orders", "prod-cn",
false,
null, null, null, 1, 20);
ArgumentCaptor<Wrapper<RmqOperationAudit>> queryCaptor =
ArgumentCaptor.forClass(Wrapper.class);
@@ -102,12 +101,32 @@ class MybatisPlusAuditRepositoryTest {
.doesNotContain("resource_name LIKE");
}
+ @Test
+ void findPageUsesEqualityForExactOperatorTest() {
+ when(auditMapper.selectPage(any(IPage.class), any(Wrapper.class)))
+ .thenReturn(new Page<RmqOperationAudit>(1,
20).setRecords(List.of()).setTotal(0));
+
+ repository.findPage(null, "ops-zhang", null, null, null, null, false,
+ null, null, null, 1, 20);
+
+ ArgumentCaptor<Wrapper<RmqOperationAudit>> queryCaptor =
ArgumentCaptor.forClass(Wrapper.class);
+ verify(auditMapper).selectPage(any(IPage.class),
queryCaptor.capture());
+ // The operator is an identity, not a fragment: a LIKE would also hand
back every
+ // "ops-zhang2" row, so one operator's filter would show another's
audit trail.
+ assertThat(queryCaptor.getValue().getSqlSegment())
+ .contains("operator =")
+ .doesNotContain("operator LIKE");
+ assertThat(((QueryWrapper<RmqOperationAudit>) queryCaptor.getValue())
+ .getParamNameValuePairs().values())
+ .contains("ops-zhang");
+ }
+
@Test
void findPageCanRestrictTimelineToRecordsWithoutAClusterTest() {
when(auditMapper.selectPage(any(IPage.class), any(Wrapper.class)))
.thenReturn(new Page<RmqOperationAudit>(1,
20).setRecords(List.of()).setTotal(0));
- repository.findPage(null, null, "SETTINGS", "general", null, true,
+ repository.findPage(null, null, null, "SETTINGS", "general", null,
true,
null, null, null, 1, 20);
ArgumentCaptor<Wrapper<RmqOperationAudit>> queryCaptor =
ArgumentCaptor.forClass(Wrapper.class);
@@ -121,7 +140,7 @@ class MybatisPlusAuditRepositoryTest {
when(auditMapper.selectPage(any(IPage.class), any(Wrapper.class)))
.thenReturn(new Page<RmqOperationAudit>(1,
20).setRecords(List.of()).setTotal(0));
- repository.findPage(null, null, "SETTINGS", "general",
"ignored-cluster", true,
+ repository.findPage(null, null, null, "SETTINGS", "general",
"ignored-cluster", true,
null, null, null, 1, 20);
ArgumentCaptor<Wrapper<RmqOperationAudit>> queryCaptor =
ArgumentCaptor.forClass(Wrapper.class);
@@ -137,16 +156,17 @@ class MybatisPlusAuditRepositoryTest {
void findFilterOptionsPreservesPersistedValuesAndCachesTheResultTest() {
when(auditMapper.selectMaps(any(Wrapper.class))).thenReturn(List.of(
Map.of("operation", "DELETE_TOPIC", "resource_type", "TOPIC",
- "cluster_id", "prod-cn", "result", "SUCCESS"),
+ "cluster_id", "prod-cn", "result", "SUCCESS",
"operator", "admin"),
Map.of("operation", " CREATE_TOPIC ", "resource_type", "GROUP",
- "cluster_id", "prod-sh", "result", "FAILED"),
+ "cluster_id", "prod-sh", "result", "FAILED",
"operator", "ops-user"),
Map.of("operation", "DELETE_TOPIC", "resource_type", "TOPIC",
- "cluster_id", "", "result", "PARTIAL")));
+ "cluster_id", "", "result", "PARTIAL", "operator",
"admin")));
AuditFilterOptionsVO options = repository.findFilterOptions();
AuditFilterOptionsVO cachedOptions = repository.findFilterOptions();
assertThat(options.getOperationTypes()).containsExactly(" CREATE_TOPIC
", "DELETE_TOPIC");
+ assertThat(options.getOperators()).containsExactly("admin",
"ops-user");
assertThat(options.getResourceTypes()).containsExactly("GROUP",
"TOPIC");
assertThat(options.getClusterIds()).containsExactly("prod-cn",
"prod-sh");
assertThat(options.getResults()).containsExactly("FAILED", "PARTIAL",
"SUCCESS");
@@ -156,7 +176,7 @@ class MybatisPlusAuditRepositoryTest {
assertThat(((QueryWrapper<RmqOperationAudit>)
queryCaptor.getValue()).getSqlSelect())
.contains("operation", "resource_type", "cluster_id",
"result");
assertThat(queryCaptor.getValue().getSqlSegment())
- .contains("GROUP BY
operation,resource_type,cluster_id,result");
+ .contains("GROUP BY
operation,resource_type,cluster_id,result,operator");
}
@Test
@@ -234,7 +254,7 @@ class MybatisPlusAuditRepositoryTest {
when(auditMapper.selectList(any(Wrapper.class))).thenReturn(List.of(latest));
AuditSummaryVO summary = repository.summarize(
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
assertThat(summary.getTotal()).isEqualTo(8);
assertThat(summary.getSuccessful()).isEqualTo(5);
@@ -284,7 +304,7 @@ class MybatisPlusAuditRepositoryTest {
when(auditMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
AuditSummaryVO summary = repository.summarize(
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
assertThat(summary.getByOperation()).extracting(AuditSummaryBucketVO::getName)
.containsExactly("op-a", "op-b", "op-c", "op-d", "op-e");
@@ -303,7 +323,7 @@ class MybatisPlusAuditRepositoryTest {
when(auditMapper.selectList(any(Wrapper.class))).thenReturn(List.of());
AuditSummaryVO summary = repository.summarize(
- null, null, null, null, null, null, null);
+ null, null, null, null, null, null, null, null);
assertThat(summary.getSuccessful()).isEqualTo(2);
assertThat(summary.getUniqueOperators()).isEqualTo(1);
@@ -322,7 +342,7 @@ class MybatisPlusAuditRepositoryTest {
LocalDateTime startDate = LocalDateTime.of(2026, 8, 1, 0, 0);
LocalDateTime endDate = LocalDateTime.of(2026, 8, 31, 23, 59);
- repository.summarize("orders", "DELETE_TOPIC", "TOPIC", "prod-cn",
+ repository.summarize("orders", "admin", "DELETE_TOPIC", "TOPIC",
"prod-cn",
startDate, endDate, "SUCCESS");
List<Wrapper<RmqOperationAudit>> queries = new java.util.ArrayList<>();
diff --git a/web/src/api/audit.ts b/web/src/api/audit.ts
index 43856c88c..22c044a0f 100644
--- a/web/src/api/audit.ts
+++ b/web/src/api/audit.ts
@@ -22,6 +22,7 @@ export type AuditFilter = Omit<AuditQuery, 'page' |
'pageSize'>;
export type AuditSummaryFilter = Omit<AuditFilter, 'target' |
'clusterIdMissing'>;
export interface AuditFilterOptions {
+ operators: string[];
operationTypes: string[];
resourceTypes: string[];
clusterIds: string[];
diff --git a/web/src/api/ops.ts b/web/src/api/ops.ts
index df6205a2a..085cb1bba 100644
--- a/web/src/api/ops.ts
+++ b/web/src/api/ops.ts
@@ -196,6 +196,7 @@ export interface AuditQuery {
page?: number;
pageSize?: number;
search?: string;
+ operator?: string;
operationType?: string;
resourceType?: string;
target?: string;
diff --git a/web/src/pages/ops/__tests__/AuditPage.test.tsx
b/web/src/pages/ops/__tests__/AuditPage.test.tsx
index 05226a79e..14e27120f 100644
--- a/web/src/pages/ops/__tests__/AuditPage.test.tsx
+++ b/web/src/pages/ops/__tests__/AuditPage.test.tsx
@@ -70,6 +70,7 @@ describe('Audit page', () => {
beforeEach(() => {
vi.mocked(opsService.getAuditFilterOptions).mockResolvedValue({
+ operators: ['admin', 'operator-a'],
operationTypes: ['ADD_PROXY_ADDRESS', 'CREATE_TOPIC', 'RESET_OFFSET'],
resourceTypes: ['CONSUMER_GROUP', 'PROXY', 'TOPIC'],
clusterIds: ['prod-cn', 'prod-sh'],
@@ -141,6 +142,7 @@ describe('Audit page', () => {
await waitFor(() =>
expect(opsService.exportAuditLogs).toHaveBeenCalledWith({
search: 'topic-a',
+ operator: undefined,
operationType: undefined,
resourceType: undefined,
clusterId: undefined,
@@ -327,6 +329,7 @@ describe('Audit page', () => {
page: 1,
pageSize: 20,
search: undefined,
+ operator: undefined,
operationType: 'CREATE_TOPIC',
resourceType: 'CONSUMER_GROUP',
clusterId: 'prod-sh',
@@ -337,6 +340,34 @@ describe('Audit page', () => {
);
});
+ it('applies the selected operator as an exact backend filter', async () => {
+ const user = userEvent.setup();
+ renderWithProviders(<AuditPage />);
+
+ expect(await screen.findByText('topic-a')).toBeInTheDocument();
+ await user.click(screen.getByRole('combobox', { name: '操作人' }));
+ await user.click(
+ await screen.findByText('operator-a', { selector:
'.ant-select-item-option-content' }),
+ );
+
+ await waitFor(() =>
+ expect(opsService.listAuditRecords).toHaveBeenLastCalledWith(
+ expect.objectContaining({ operator: 'operator-a' }),
+ ),
+ );
+ await waitFor(() =>
+ expect(opsService.getAuditSummary).toHaveBeenLastCalledWith(
+ expect.objectContaining({ operator: 'operator-a' }),
+ ),
+ );
+ await user.click(screen.getByRole('button', { name: /导出/ }));
+ await waitFor(() =>
+ expect(opsService.exportAuditLogs).toHaveBeenLastCalledWith(
+ expect.objectContaining({ operator: 'operator-a' }),
+ ),
+ );
+ });
+
it('shows loading state while refreshed records are pending', async () => {
const user = userEvent.setup();
vi.mocked(opsService.listAuditRecords)
@@ -455,6 +486,7 @@ describe('Audit page', () => {
await act(async () => {
staleOptions.resolve({
+ operators: ['STALE_OPERATOR'],
operationTypes: ['STALE_OPERATION'],
resourceTypes: [],
clusterIds: [],
diff --git a/web/src/pages/ops/audit.tsx b/web/src/pages/ops/audit.tsx
index 62827b271..9f79e1077 100644
--- a/web/src/pages/ops/audit.tsx
+++ b/web/src/pages/ops/audit.tsx
@@ -65,6 +65,7 @@ import ResourceOperationTimelineModal, {
} from './ResourceOperationTimelineModal';
const emptyFilterOptions: AuditFilterOptions = {
+ operators: [],
operationTypes: [],
resourceTypes: [],
clusterIds: [],
@@ -73,6 +74,7 @@ const emptyFilterOptions: AuditFilterOptions = {
const buildAuditFilter = (
searchText: string,
+ selectedOperator: string | undefined,
selectedType: string | undefined,
selectedResourceType: string | undefined,
selectedClusterId: string | undefined,
@@ -80,6 +82,7 @@ const buildAuditFilter = (
resultFilter: string,
): AuditFilter => ({
search: searchText || undefined,
+ operator: selectedOperator,
operationType: selectedType,
resourceType: selectedResourceType,
clusterId: selectedClusterId,
@@ -98,6 +101,7 @@ const AuditPage: React.FC = () => {
const [refreshKey, setRefreshKey] = useState(0);
const [searchText, setSearchText] = useState('');
const [debouncedSearchText, setDebouncedSearchText] = useState('');
+ const [selectedOperator, setSelectedOperator] = useState<string |
undefined>(undefined);
const [selectedType, setSelectedType] = useState<string |
undefined>(undefined);
const [selectedResourceType, setSelectedResourceType] = useState<string |
undefined>(undefined);
const [selectedClusterId, setSelectedClusterId] = useState<string |
undefined>(undefined);
@@ -147,6 +151,7 @@ const AuditPage: React.FC = () => {
pageSize,
...buildAuditFilter(
debouncedSearchText,
+ selectedOperator,
selectedType,
selectedResourceType,
selectedClusterId,
@@ -177,6 +182,7 @@ const AuditPage: React.FC = () => {
page,
pageSize,
debouncedSearchText,
+ selectedOperator,
selectedType,
selectedResourceType,
selectedClusterId,
@@ -198,6 +204,7 @@ const AuditPage: React.FC = () => {
() =>
buildAuditFilter(
debouncedSearchText,
+ selectedOperator,
selectedType,
selectedResourceType,
selectedClusterId,
@@ -206,6 +213,7 @@ const AuditPage: React.FC = () => {
),
[
debouncedSearchText,
+ selectedOperator,
selectedType,
selectedResourceType,
selectedClusterId,
@@ -453,6 +461,18 @@ const AuditPage: React.FC = () => {
style={{ width: 240 }}
allowClear
/>
+ <Select
+ aria-label={t('audit.operator')}
+ placeholder={t('audit.operator')}
+ allowClear
+ style={{ width: 150 }}
+ value={selectedOperator}
+ onChange={(value) => {
+ setPage(1);
+ setSelectedOperator(value);
+ }}
+ options={filterOptions.operators.map((value) => ({ label: value,
value }))}
+ />
<Select
aria-label={t('audit.opType')}
placeholder={t('audit.opType')}
diff --git a/web/src/services/opsService.ts b/web/src/services/opsService.ts
index 390742a71..6ebc07917 100644
--- a/web/src/services/opsService.ts
+++ b/web/src/services/opsService.ts
@@ -71,6 +71,7 @@ function distinctSorted(values: Array<string | null |
undefined>): string[] {
function getMockAuditFilterOptions(): AuditFilterOptions {
return {
+ operators: distinctSorted(auditRecordsState.map((record) =>
record.operator)),
operationTypes: distinctSorted(auditRecordsState.map((record) =>
record.operationType)),
resourceTypes: distinctSorted(auditRecordsState.map((record) =>
record.resourceType)),
clusterIds: distinctSorted(auditRecordsState.map((record) =>
record.clusterId)),
@@ -89,6 +90,7 @@ function filterAuditRecords(params: AuditFilter):
AuditRecord[] {
) {
return false;
}
+ if (params.operator && record.operator !== params.operator) return false;
if (params.operationType && record.operationType !== params.operationType)
return false;
if (params.resourceType && record.resourceType !== params.resourceType)
return false;
if (params.target && record.target !== params.target) return false;