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 d687a9c94 fix(ops): disable writes until configuration reload succeeds
(#5359)
d687a9c94 is described below
commit d687a9c942fef7e04d1f6038b86ac7dc8c072b41
Author: coder999o <[email protected]>
AuthorDate: Fri Oct 9 11:25:16 2026 +0800
fix(ops): disable writes until configuration reload succeeds (#5359)
`writeOperationEnabled` ignored `loadState` and was never re-armed, so the
VIP/TLS switches stayed
clickable while the page was showing a load failure, and a
language-triggered refetch did not put
the flag back to loading either. Gate the flag on `loadState === 'ready'`
and reset it to loading at
the start of every effect run; this covers all eight write-control
consumers.
---
web/src/pages/studio/Ops.tsx | 6 ++-
web/src/pages/studio/__tests__/Ops.test.tsx | 58 +++++++++++++++++++++++++++++
2 files changed, 63 insertions(+), 1 deletion(-)
diff --git a/web/src/pages/studio/Ops.tsx b/web/src/pages/studio/Ops.tsx
index 5d84e12dc..04ed60811 100644
--- a/web/src/pages/studio/Ops.tsx
+++ b/web/src/pages/studio/Ops.tsx
@@ -68,12 +68,16 @@ const OpsPage: React.FC = () => {
*/
const [loadState, setLoadState] = useState<'loading' | 'ready' |
'failed'>('loading');
const [reloadKey, setReloadKey] = useState(0);
- const writeOperationEnabled = configurationAvailable && (!userId || admin
=== true);
+ const writeOperationEnabled =
+ loadState === 'ready' && configurationAvailable && (!userId || admin ===
true);
const deleteNameServerDisabled =
!selectedNamesrv || selectedNamesrv === currentNamesrv ||
namesrvAddrList.length <= 1;
useEffect(() => {
let cancelled = false;
+ void Promise.resolve().then(() => {
+ if (!cancelled) setLoadState('loading');
+ });
const loadOpsData = async () => {
try {
diff --git a/web/src/pages/studio/__tests__/Ops.test.tsx
b/web/src/pages/studio/__tests__/Ops.test.tsx
index c0a11e2df..9ce988f89 100644
--- a/web/src/pages/studio/__tests__/Ops.test.tsx
+++ b/web/src/pages/studio/__tests__/Ops.test.tsx
@@ -180,6 +180,64 @@ describe('OpsPage', () => {
expect(screen.queryByRole('button', { name: /重\s*试|Retry/
})).not.toBeInTheDocument();
});
+ it('disables writes when a configuration reload fails after a successful
load', async () => {
+ const user = userEvent.setup();
+ renderWithProviders(
+ <>
+ <LanguageSwitch />
+ <OpsPage />
+ </>,
+ );
+
+ expect(await
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+ vi.mocked(queryOpsHomePage).mockRejectedValueOnce(new Error('network
down'));
+ await user.click(screen.getByRole('button', { name: 'switch-language' }));
+
+ expect(await screen.findByRole('button', { name: /Retry/
})).toBeInTheDocument();
+
expect(screen.queryByPlaceholderText('NamesrvAddr')).not.toBeInTheDocument();
+ screen.getAllByRole('switch').forEach((toggle) =>
expect(toggle).toBeDisabled());
+
+ await user.click(screen.getByRole('button', { name: /Retry/ }));
+ expect(await
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+ screen.getAllByRole('switch').forEach((toggle) =>
expect(toggle).toBeEnabled());
+ });
+
+ it('disables writes while a configuration reload is pending', async () => {
+ const user = userEvent.setup();
+ renderWithProviders(
+ <>
+ <LanguageSwitch />
+ <OpsPage />
+ </>,
+ );
+
+ expect(await
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+ let resolveReload!: (value: Awaited<ReturnType<typeof queryOpsHomePage>>)
=> void;
+ vi.mocked(queryOpsHomePage).mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveReload = resolve;
+ }),
+ );
+ await user.click(screen.getByRole('button', { name: 'switch-language' }));
+ await waitFor(() => expect(queryOpsHomePage).toHaveBeenCalledTimes(2));
+
+
expect(screen.queryByPlaceholderText('NamesrvAddr')).not.toBeInTheDocument();
+ screen.getAllByRole('switch').forEach((toggle) =>
expect(toggle).toBeDisabled());
+
+ await act(async () => {
+ resolveReload({
+ namesvrAddrList: ['127.0.0.1:9876', '127.0.0.2:9876'],
+ configurationAvailable: true,
+ useVIPChannel: true,
+ useTLS: false,
+ currentNamesrv: '127.0.0.1:9876',
+ });
+ });
+ expect(await
screen.findByPlaceholderText('NamesrvAddr')).toBeInTheDocument();
+ screen.getAllByRole('switch').forEach((toggle) =>
expect(toggle).toBeEnabled());
+ });
+
it('hides write controls for read-only users', async () => {
useAuthStore.setState({ user: 'reader', userId: 101, admin: false });