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 bf743cc4b fix(alert): surface import conflict details (#4334)
bf743cc4b is described below
commit bf743cc4b0d1f4ddb14d6c9f0950f3353332c06e
Author: aias00 <[email protected]>
AuthorDate: Wed Sep 16 16:09:59 2026 +0800
fix(alert): surface import conflict details (#4334)
Signed-off-by: liuhy <[email protected]>
---
.../rocketmq/studio/ops/alert/AlertService.java | 29 ++++++++++++++++------
.../studio/ops/alert/AlertServiceTest.java | 4 +--
web/src/pages/instance/index.tsx | 7 +-----
web/src/pages/ops/__tests__/AlertsPage.test.tsx | 24 ++++++++++++++++++
web/src/pages/ops/alerts.tsx | 5 ++--
web/src/utils/apiError.ts | 9 +++++++
6 files changed, 60 insertions(+), 18 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java
index 1f2aa3f4f..4d8b7ef8c 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/ops/alert/AlertService.java
@@ -227,12 +227,9 @@ public class AlertService {
}
private void rejectDuplicateSemanticRule(AlertRuleVO rule, Long
excludedId) {
- String fingerprint = AlertRuleSemanticFingerprint.of(rule);
- boolean duplicate = alertRepository.findAllRules().stream()
- .filter(candidate -> !Objects.equals(candidate.getId(),
excludedId))
- .anyMatch(candidate ->
AlertRuleSemanticFingerprint.of(candidate).equals(fingerprint));
- if (duplicate) {
- throw new BusinessException(409, "An alert rule with the same
evaluation conditions already exists");
+ AlertRuleVO duplicate = findDuplicateSemanticRule(rule, excludedId);
+ if (duplicate != null) {
+ throw duplicateRuleException(duplicate);
}
}
@@ -240,7 +237,7 @@ public class AlertService {
try {
return alertRepository.insertRule(rule);
} catch (DuplicateKeyException duplicate) {
- throw new BusinessException(409, "An alert rule with the same
evaluation conditions already exists");
+ throw duplicateRuleException(findDuplicateSemanticRule(rule,
null));
}
}
@@ -248,10 +245,26 @@ public class AlertService {
try {
return alertRepository.replaceRule(rule);
} catch (DuplicateKeyException duplicate) {
- throw new BusinessException(409, "An alert rule with the same
evaluation conditions already exists");
+ throw duplicateRuleException(findDuplicateSemanticRule(rule,
rule.getId()));
}
}
+ private AlertRuleVO findDuplicateSemanticRule(AlertRuleVO rule, Long
excludedId) {
+ String fingerprint = AlertRuleSemanticFingerprint.of(rule);
+ return alertRepository.findAllRules().stream()
+ .filter(candidate -> !Objects.equals(candidate.getId(),
excludedId))
+ .filter(candidate ->
AlertRuleSemanticFingerprint.of(candidate).equals(fingerprint))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static BusinessException duplicateRuleException(AlertRuleVO
existing) {
+ String suffix = existing != null &&
StringUtils.hasText(existing.getName())
+ ? ": " + existing.getName() : "";
+ return new BusinessException(409,
+ "An alert rule with the same evaluation conditions already
exists" + suffix);
+ }
+
private void requireDomain(AlertDomain domain) {
if (domain == null) {
throw new BusinessException(400, "Alert domain is required");
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
index 46c95023b..02a32d34b 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/ops/alert/AlertServiceTest.java
@@ -996,7 +996,7 @@ class AlertServiceTest {
assertThatThrownBy(() -> alertService.createRule(duplicate))
.isInstanceOf(BusinessException.class)
- .hasMessage("An alert rule with the same evaluation conditions
already exists")
+ .hasMessage("An alert rule with the same evaluation conditions
already exists: Existing rule")
.satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(409));
verify(alertRepository, never()).insertRule(any());
@@ -1030,7 +1030,7 @@ class AlertServiceTest {
assertThatThrownBy(() -> alertService.updateRule(update))
.isInstanceOf(BusinessException.class)
- .hasMessage("An alert rule with the same evaluation conditions
already exists")
+ .hasMessage("An alert rule with the same evaluation conditions
already exists: Existing")
.satisfies(error -> assertThat(((BusinessException)
error).getCode()).isEqualTo(409));
verify(alertRepository, never()).replaceRule(any());
diff --git a/web/src/pages/instance/index.tsx b/web/src/pages/instance/index.tsx
index 146013c65..0061354c5 100644
--- a/web/src/pages/instance/index.tsx
+++ b/web/src/pages/instance/index.tsx
@@ -47,6 +47,7 @@ import {
type CloudRegion,
} from '../../api/aliyunCatalog';
import { listTencentInstances, listTencentRegions } from
'../../api/tencentCatalog';
+import { describeApiError } from '../../utils/apiError';
import { formatDateTime } from '../../utils/format';
import { tableScrollX } from '../../utils/table';
import {
@@ -92,12 +93,6 @@ const APACHE_ACCESS_TYPE_OPTIONS = [
{ value: 'DIRECT', labelKey: 'instance.directMode' },
] as const;
-function describeApiError(error: unknown, fallback: string): string {
- const serverMessage = (error as { response?: { data?: { message?: unknown }
} })?.response?.data
- ?.message;
- return typeof serverMessage === 'string' && serverMessage.trim() ?
serverMessage : fallback;
-}
-
type InstanceTypeFilter = 'ALL' | Instance['type'];
function compareResourceCounts(
diff --git a/web/src/pages/ops/__tests__/AlertsPage.test.tsx
b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
index 9f4f34431..0291f2ace 100644
--- a/web/src/pages/ops/__tests__/AlertsPage.test.tsx
+++ b/web/src/pages/ops/__tests__/AlertsPage.test.tsx
@@ -31,6 +31,7 @@ import {
listAlertRulesPage,
listAlertRuleRuntime,
listNativeAlertMetrics,
+ importAlertRulesTransfer,
toggleAlertRule,
} from '../../../services/opsService';
@@ -758,6 +759,29 @@ describe('AlertsPage', () => {
);
});
+ it('shows an actionable server error when alert rule import conflicts',
async () => {
+ const serverMessage =
+ 'An alert rule with the same evaluation conditions already exists:
Existing rule';
+ vi.mocked(importAlertRulesTransfer).mockRejectedValue({
+ response: { data: { code: 409, message: serverMessage } },
+ });
+ const user = userEvent.setup();
+ const { container } = renderPage();
+ await screen.findByText('Broker disk usage');
+ const input =
container.querySelector<HTMLInputElement>('input[type="file"]');
+ if (!input) throw new Error('Alert rule import input not found');
+ const file = new File(
+ [JSON.stringify({ version: 1, domain: 'CLUSTER', rules: [] })],
+ 'cluster-alert-rules.json',
+ { type: 'application/json' },
+ );
+
+ await user.upload(input, file);
+
+ expect(await screen.findByText(serverMessage)).toBeInTheDocument();
+ expect(screen.queryByText('导入失败,请选择当前页面导出的规则文件')).not.toBeInTheDocument();
+ });
+
it('disables other alert rule mutations while a bulk action is running',
async () => {
let resolveToggle:
| ((result: {
diff --git a/web/src/pages/ops/alerts.tsx b/web/src/pages/ops/alerts.tsx
index d88c97876..5b6085360 100644
--- a/web/src/pages/ops/alerts.tsx
+++ b/web/src/pages/ops/alerts.tsx
@@ -76,6 +76,7 @@ import {
type AlertTemplatePreviewIssue,
} from '../../utils/alertTemplatePreview';
import type { TextAreaRef } from 'antd/es/input/TextArea';
+import { describeApiError } from '../../utils/apiError';
const { TextArea } = Input;
const channelColors: Record<string, string> = {
@@ -448,8 +449,8 @@ const AlertsPage = ({ domain = 'CLUSTER' }:
AlertsPageProps) => {
setPage(1);
refreshRules();
message.success(t('alerts.importSuccess', { count: imported.length }));
- } catch {
- message.error(t('alerts.importFailed'));
+ } catch (error) {
+ message.error(describeApiError(error, t('alerts.importFailed')));
} finally {
setTransferringRules(false);
}
diff --git a/web/src/utils/apiError.ts b/web/src/utils/apiError.ts
new file mode 100644
index 000000000..2d59c8217
--- /dev/null
+++ b/web/src/utils/apiError.ts
@@ -0,0 +1,9 @@
+/**
+ * Extracts the server-supplied message from an API error so callers can
surface the
+ * concrete rejection reason instead of a generic fallback.
+ */
+export function describeApiError(error: unknown, fallback: string): string {
+ const serverMessage = (error as { response?: { data?: { message?: unknown }
} })?.response?.data
+ ?.message;
+ return typeof serverMessage === 'string' && serverMessage.trim() ?
serverMessage : fallback;
+}