You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by li...@apache.org on 2026/10/09 03:25:21 UTC
(rocketmq-dashboard) branch rocketmq-studio updated: fix(ops): disable writes until configuration reload succeeds (#5359)
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 <so...@126.com>
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 });