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 c4eae142 fix: remove simulated operational write success (#938)
c4eae142 is described below
commit c4eae1426a952747703d882f2e60c0aa45c8afb2
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 4 03:05:40 2026 -0700
fix: remove simulated operational write success (#938)
* fix: disable simulated message resend action
* [ISSUE #788] Return unavailable for Ops setting writes
* fix(web): stop simulating SSL settings saves
* fix: stop fake consume verification success
---
.../org/apache/rocketmq/studio/ops/OpsService.java | 39 ++++++--------
.../rocketmq/studio/ops/OpsControllerTest.java | 18 +++++++
.../apache/rocketmq/studio/ops/OpsServiceTest.java | 61 ++++++++--------------
web/src/i18n/translations.ts | 4 ++
.../pages/instance/__tests__/MessagePage.test.tsx | 31 +++++++++++
.../__tests__/MessagePageAsyncState.test.tsx | 15 ++++++
web/src/pages/instance/message.tsx | 15 ++++--
web/src/pages/studio/SslSettings.tsx | 14 ++---
.../pages/studio/__tests__/SslSettings.test.tsx | 20 +++++++
9 files changed, 140 insertions(+), 77 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsService.java
b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsService.java
index 57ddd095..e21408d3 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/ops/OpsService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/ops/OpsService.java
@@ -30,6 +30,9 @@ import java.util.Set;
@Service
public class OpsService {
+ private static final String OPS_SETTINGS_UNAVAILABLE =
+ "Ops settings are not connected to the cluster admin
configuration";
+
private final Set<String> namesrvAddrs = new
LinkedHashSet<>(List.of("127.0.0.1:9876"));
private String currentNamesrv = "127.0.0.1:9876";
private boolean useVIPChannel = true;
@@ -45,41 +48,26 @@ public class OpsService {
}
public synchronized void updateNameServer(String namesrvAddr) {
- String normalized = normalizeNameServer(namesrvAddr);
- namesrvAddrs.add(normalized);
- currentNamesrv = normalized;
- log.info("Updated current NameServer address to {}", normalized);
+ normalizeNameServer(namesrvAddr);
+ throw settingsUnavailable();
}
public synchronized void addNameServer(String namesrvAddr) {
- String normalized = normalizeNameServer(namesrvAddr);
- namesrvAddrs.add(normalized);
- log.info("Added NameServer address {}", normalized);
+ normalizeNameServer(namesrvAddr);
+ throw settingsUnavailable();
}
public synchronized void deleteNameServer(String namesrvAddr) {
- String normalized = normalizeNameServer(namesrvAddr);
- if (!namesrvAddrs.contains(normalized)) {
- throw new BusinessException(404, "NameServer address not found: "
+ normalized);
- }
- if (namesrvAddrs.size() == 1) {
- throw new BusinessException(409, "Cannot delete the last
NameServer address");
- }
- if (normalized.equals(currentNamesrv)) {
- throw new BusinessException(409, "Cannot delete the current
NameServer address");
- }
- namesrvAddrs.remove(normalized);
- log.info("Deleted NameServer address {}", normalized);
+ normalizeNameServer(namesrvAddr);
+ throw settingsUnavailable();
}
public synchronized void updateVipChannel(boolean enabled) {
- useVIPChannel = enabled;
- log.info("Updated VIP channel setting to {}", enabled);
+ throw settingsUnavailable();
}
public synchronized void updateUseTLS(boolean enabled) {
- useTLS = enabled;
- log.info("Updated TLS setting to {}", enabled);
+ throw settingsUnavailable();
}
private String normalizeNameServer(String namesrvAddr) {
@@ -88,4 +76,9 @@ public class OpsService {
}
return namesrvAddr.trim();
}
+
+ private BusinessException settingsUnavailable() {
+ log.warn(OPS_SETTINGS_UNAVAILABLE);
+ return new BusinessException(501, OPS_SETTINGS_UNAVAILABLE);
+ }
}
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 b8e2ee96..7ea52110 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
@@ -18,6 +18,7 @@
package org.apache.rocketmq.studio.ops;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
@@ -30,6 +31,7 @@ import java.util.List;
import java.util.Map;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
@@ -81,6 +83,22 @@ class OpsControllerTest {
verify(opsService).updateNameServer(eq("10.0.0.1:9876"));
}
+ @Test
+ void updateNameSvrAddrShouldReturnUnavailableWhenServiceRejects() throws
Exception {
+ doThrow(new BusinessException(501, "Ops settings are not connected to
the cluster admin configuration"))
+ .when(opsService).updateNameServer("10.0.0.1:9876");
+
+ mockMvc.perform(post("/api/ops/updateNameSvrAddr")
+ .contentType(MediaType.APPLICATION_JSON)
+
.content(objectMapper.writeValueAsString(Map.of("namesrvAddr",
"10.0.0.1:9876"))))
+ .andExpect(status().isNotImplemented())
+ .andExpect(jsonPath("$.code").value(501))
+ .andExpect(jsonPath("$.message").value(
+ "Ops settings are not connected to the cluster admin
configuration"));
+
+ verify(opsService).updateNameServer(eq("10.0.0.1:9876"));
+ }
+
@Test
void updateNameSvrAddrShouldRejectMissingAddress() throws Exception {
mockMvc.perform(post("/api/ops/updateNameSvrAddr")
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/ops/OpsServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/ops/OpsServiceTest.java
index 15e24667..1035a8e7 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/ops/OpsServiceTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/ops/OpsServiceTest.java
@@ -38,22 +38,19 @@ class OpsServiceTest {
}
@Test
- void addAndUpdateNameServerShouldMaintainUniqueAddressList() {
- opsService.addNameServer(" 10.0.0.1:9876 ");
- opsService.addNameServer("10.0.0.1:9876");
- opsService.updateNameServer("10.0.0.2:9876");
-
- OpsHomeVO home = opsService.getHomePage();
- assertThat(home.getNamesvrAddrList())
- .containsExactly("127.0.0.1:9876", "10.0.0.1:9876",
"10.0.0.2:9876");
- assertThat(home.getCurrentNamesrv()).isEqualTo("10.0.0.2:9876");
- }
-
- @Test
- void deleteNameServerShouldRemoveNonCurrentAddress() {
- opsService.addNameServer("10.0.0.1:9876");
-
- opsService.deleteNameServer(" 10.0.0.1:9876 ");
+ void nameServerWritesShouldReturnUnavailable() {
+ assertThatThrownBy(() -> opsService.addNameServer(" 10.0.0.1:9876 "))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("Ops settings are not connected to the cluster
admin configuration")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(501));
+ assertThatThrownBy(() -> opsService.updateNameServer("10.0.0.2:9876"))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("Ops settings are not connected to the cluster
admin configuration")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(501));
+ assertThatThrownBy(() -> opsService.deleteNameServer("127.0.0.1:9876"))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("Ops settings are not connected to the cluster
admin configuration")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(501));
OpsHomeVO home = opsService.getHomePage();
assertThat(home.getNamesvrAddrList()).containsExactly("127.0.0.1:9876");
@@ -61,33 +58,19 @@ class OpsServiceTest {
}
@Test
- void deleteNameServerShouldRejectUnknownCurrentAndLastAddress() {
- assertThatThrownBy(() -> opsService.deleteNameServer("10.0.0.1:9876"))
+ void togglesShouldReturnUnavailable() {
+ assertThatThrownBy(() -> opsService.updateVipChannel(false))
.isInstanceOf(BusinessException.class)
- .hasMessage("NameServer address not found: 10.0.0.1:9876")
- .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(404));
-
- assertThatThrownBy(() -> opsService.deleteNameServer("127.0.0.1:9876"))
+ .hasMessage("Ops settings are not connected to the cluster
admin configuration")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(501));
+ assertThatThrownBy(() -> opsService.updateUseTLS(true))
.isInstanceOf(BusinessException.class)
- .hasMessage("Cannot delete the last NameServer address")
- .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(409));
-
- opsService.addNameServer("10.0.0.1:9876");
- opsService.updateNameServer("10.0.0.1:9876");
- assertThatThrownBy(() -> opsService.deleteNameServer("10.0.0.1:9876"))
- .isInstanceOf(BusinessException.class)
- .hasMessage("Cannot delete the current NameServer address")
- .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(409));
- }
-
- @Test
- void togglesShouldUpdateHomePageSettings() {
- opsService.updateVipChannel(false);
- opsService.updateUseTLS(true);
+ .hasMessage("Ops settings are not connected to the cluster
admin configuration")
+ .satisfies(ex -> assertThat(((BusinessException)
ex).getCode()).isEqualTo(501));
OpsHomeVO home = opsService.getHomePage();
- assertThat(home.isUseVIPChannel()).isFalse();
- assertThat(home.isUseTLS()).isTrue();
+ assertThat(home.isUseVIPChannel()).isTrue();
+ assertThat(home.isUseTLS()).isFalse();
}
@Test
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index 3838f340..fd7f40c8 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1076,6 +1076,10 @@ const translations: Record<string, Record<Lang, string>>
= {
'ssl.expiryDate': { zh: '过期日期', en: 'Expiry Date' },
'ssl.active': { zh: '有效', en: 'Active' },
'ssl.saveSuccess': { zh: 'SSL 配置保存成功', en: 'SSL configuration saved
successfully' },
+ 'ssl.saveUnavailable': {
+ zh: 'SSL 配置保存功能尚未接入真实后端接口',
+ en: 'SSL configuration persistence is not wired to a backend API yet',
+ },
'ssl.invalidCertFormat': { zh: '仅允许证书文件!', en: 'Only certificate files are
allowed!' },
'ssl.certRemoved': { zh: '证书文件已移除', en: 'Certificate file removed' },
diff --git a/web/src/pages/instance/__tests__/MessagePage.test.tsx
b/web/src/pages/instance/__tests__/MessagePage.test.tsx
index 630ae17b..290a8116 100644
--- a/web/src/pages/instance/__tests__/MessagePage.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePage.test.tsx
@@ -41,6 +41,19 @@ vi.mock('../../../services/topicService', () => ({
import MessagePage from '../message';
+const createMessage = (msgId: string) => ({
+ msgId,
+ topic: `topic-${msgId}`,
+ tag: 'tag',
+ key: `key-${msgId}`,
+ body: '{}',
+ storeTime: '2026-07-31T00:00:00Z',
+ bornHost: '127.0.0.1:1000',
+ storeHost: '127.0.0.1:10911',
+ properties: {},
+ size: 2,
+});
+
beforeAll(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
@@ -222,4 +235,22 @@ describe('Message page query history', () => {
expect(screen.queryByText(/invalid/)).not.toBeInTheDocument();
expect(screen.queryByText(/order-create/)).not.toBeInTheDocument();
});
+
+ it('does not report consume verification success without a backend API',
async () => {
+ const user = userEvent.setup();
+
messageServiceMocks.queryMessages.mockResolvedValue([createMessage('MID-CONSUME-VERIFY-001')]);
+ renderWithProviders(<MessagePage />);
+
+ await user.click(screen.getByText('按 Message ID'));
+ await user.type(screen.getByPlaceholderText('输入 Message ID'),
'MID-CONSUME-VERIFY-001');
+ await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+
+ expect(await
screen.findByText('MID-CONSUME-VERIFY-001')).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: /验证/ }));
+
+ expect(
+ await screen.findByText('消费验证接口尚未接入,无法确认该消息的真实消费状态'),
+ ).toBeInTheDocument();
+ expect(screen.queryByText(/消费验证成功/)).not.toBeInTheDocument();
+ });
});
diff --git a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
index 2f938b4b..a2ebe31a 100644
--- a/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
+++ b/web/src/pages/instance/__tests__/MessagePageAsyncState.test.tsx
@@ -131,6 +131,21 @@ describe('MessagePage async request ownership', () => {
expect(screen.queryByText('late-after-reset')).not.toBeInTheDocument();
});
+ it('keeps normal message resend disabled until a real API is wired', async
() => {
+ serviceMocks.queryMessages.mockResolvedValue([createMessage('message-a')]);
+ const user = userEvent.setup();
+ renderPage();
+
+ await user.click(screen.getByRole('button', { name: /^search查询$/ }));
+ const row = await screen.findByRole('row', { name: /message-a/ });
+ await user.click(within(row).getByRole('button', { name: /详情/ }));
+
+ const dialog = await screen.findByRole('dialog', { name: '消息详情' });
+ const resendButton = within(dialog).getByRole('button', { name: /重新发送/ });
+ expect(resendButton).toBeDisabled();
+ expect(resendButton).toHaveAttribute('title', '当前版本尚未接入普通消息重新发送接口');
+ });
+
it('keeps the latest query loading and ignores an earlier query result',
async () => {
const firstQuery = createDeferred<MessageRecord[]>();
const secondQuery = createDeferred<MessageRecord[]>();
diff --git a/web/src/pages/instance/message.tsx
b/web/src/pages/instance/message.tsx
index e200fd9f..ee56b2b6 100644
--- a/web/src/pages/instance/message.tsx
+++ b/web/src/pages/instance/message.tsx
@@ -72,6 +72,7 @@ type RecentQuery = {
const QUERY_HISTORY_STORAGE_KEY = 'rocketmq-studio-message-query-history';
const MAX_QUERY_HISTORY = 5;
+const RESEND_UNAVAILABLE_MESSAGE = '当前版本尚未接入普通消息重新发送接口';
const QUERY_OPTIONS = [
{ value: 'topic' as const, label: '按 Topic 查询' },
@@ -348,10 +349,9 @@ const MessagePage = () => {
if (recentQuery) replayRecentQuery(recentQuery);
};
- const handleResend = () => {
- message.success('消息重新发送成功(模拟)');
+ const handleVerifyConsume = () => {
+ message.warning('消费验证接口尚未接入,无法确认该消息的真实消费状态');
};
-
const openDetail = async (record: MessageRecord, tab = 'content') => {
const requestGeneration = traceGenerationRef.current + 1;
traceGenerationRef.current = requestGeneration;
@@ -475,7 +475,7 @@ const MessagePage = () => {
size="small"
icon={<CheckCircleOutlined />}
style={{ borderColor: '#52c41a', color: '#52c41a' }}
- onClick={() => message.success(`消息 ${record.msgId.slice(0, 16)}...
消费验证成功`)}
+ onClick={handleVerifyConsume}
>
验证
</Button>
@@ -774,7 +774,12 @@ const MessagePage = () => {
footer={
<Flex justify="flex-end" gap={8}>
<Button onClick={closeDetail}>关闭</Button>
- <Button type="primary" icon={<SendOutlined />}
onClick={handleResend}>
+ <Button
+ type="primary"
+ icon={<SendOutlined />}
+ disabled
+ title={RESEND_UNAVAILABLE_MESSAGE}
+ >
重新发送
</Button>
</Flex>
diff --git a/web/src/pages/studio/SslSettings.tsx
b/web/src/pages/studio/SslSettings.tsx
index 26b77a64..9ed498e1 100644
--- a/web/src/pages/studio/SslSettings.tsx
+++ b/web/src/pages/studio/SslSettings.tsx
@@ -66,8 +66,7 @@ interface FormValues {
// ─── Component ──────────────────────────────────────────────────
const SslSettingsPage = () => {
const [form] = Form.useForm<FormValues>();
- const [loading, setLoading] = useState(false);
- const [sslConfig, setSslConfig] = useState<SslConfig>({
+ const [sslConfig] = useState<SslConfig>({
enabled: false,
protocol: 'TLSv1.3',
keyStoreType: 'JKS',
@@ -85,13 +84,8 @@ const SslSettingsPage = () => {
const { t } = useLang();
const { message } = App.useApp();
- const handleSave = (values: FormValues) => {
- setLoading(true);
- setTimeout(() => {
- setLoading(false);
- message.success(t('ssl.saveSuccess'));
- setSslConfig({ ...sslConfig, ...values });
- }, 1000);
+ const handleSave = () => {
+ message.error(t('ssl.saveUnavailable'));
};
const uploadProps = {
@@ -276,7 +270,7 @@ const SslSettingsPage = () => {
<Form.Item>
<Space>
- <Button type="primary" htmlType="submit" loading={loading}>
+ <Button type="primary" htmlType="submit">
{t('ssl.save')}
</Button>
<Button onClick={() =>
form.setFieldsValue(sslConfig)}>{t('common.reset')}</Button>
diff --git a/web/src/pages/studio/__tests__/SslSettings.test.tsx
b/web/src/pages/studio/__tests__/SslSettings.test.tsx
index a921c7e8..dac2ac1d 100644
--- a/web/src/pages/studio/__tests__/SslSettings.test.tsx
+++ b/web/src/pages/studio/__tests__/SslSettings.test.tsx
@@ -124,6 +124,26 @@ describe('SslSettings Page', () => {
expect(screen.getByRole('button', { name: /重\s*置/ })).toBeInTheDocument();
});
+ it('does not persist SSL changes when the backend API is unavailable', async
() => {
+ const user = userEvent.setup();
+ renderWithProviders(<SslSettings />);
+
+ const switchEl = screen.getByRole('switch');
+ await user.click(switchEl);
+ await user.type(screen.getByLabelText('KeyStore 路径'),
'/etc/rocketmq/keystore.jks');
+ await user.type(screen.getByLabelText('KeyStore 密码'), 'changeit');
+
+ await user.click(screen.getByRole('button', { name: /保\s*存/ }));
+
+ expect(await screen.findByText('SSL
配置保存功能尚未接入真实后端接口')).toBeInTheDocument();
+ expect(screen.queryByText('SSL 配置保存成功')).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole('button', { name: /重\s*置/ }));
+
+ expect(switchEl).not.toBeChecked();
+ expect(screen.queryByText('KeyStore 配置')).not.toBeInTheDocument();
+ });
+
it('should show TrustStore fields when client authentication is required',
async () => {
const user = userEvent.setup();
renderWithProviders(<SslSettings />);