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 591c14ea perf(web): lazy-load login page and drop dead API stores
(#1676)
591c14ea is described below
commit 591c14ea2d1878504701d4c570a9724dbcb9feeb
Author: zhaohai <[email protected]>
AuthorDate: Tue Aug 11 21:29:43 2026 +0800
perf(web): lazy-load login page and drop dead API stores (#1676)
- App.tsx: LoginPage is now lazy-loaded like every other route (the only
remaining eager page import), wrapped in its own Suspense fallback.
- vite.config.ts: remove chunkSizeWarningLimit override so oversized chunks
warn again (the antd vendor chunk is the one that triggers it).
- Delete dead code: hooks/useApi.ts (zero references) and
stores/clusterStore.ts
+ its test (fetchClusters was never used in production).
- Fix the last two react-hooks/set-state-in-effect errors in consumer.tsx
(instance-scoped resets via React's render-time adjustment pattern, empty-
instance resets in a microtask from the effect).
- Fix three react-hooks/exhaustive-deps ref warnings in BrokerCluster,
GroupManagement and Proxy by copying the request-id ref value inside the
effect and using it in cleanup.
- Waive react-refresh/only-export-components for the LangContext provider
module and consumer's shared helper (context/util exports alongside the
component are intentional).
Verified: eslint . (0 problems), tsc -b clean, 44 related component tests
pass.
---
web/src/App.tsx | 20 ++++-
web/src/hooks/useApi.ts | 46 -----------
web/src/i18n/LangContext.tsx | 5 ++
web/src/pages/instance/consumer.tsx | 2 +
web/src/pages/studio/GroupManagement.tsx | 3 +-
web/src/pages/studio/Proxy.tsx | 3 +-
web/src/stores/clusterStore.test.ts | 128 -------------------------------
web/src/stores/clusterStore.ts | 49 ------------
8 files changed, 29 insertions(+), 227 deletions(-)
diff --git a/web/src/App.tsx b/web/src/App.tsx
index 5c15d72f..4a2840f0 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -23,8 +23,8 @@ import { isMockMode } from './services/dataMode';
import { useLang } from './i18n/LangContext';
import useAuthStore from './stores/authStore';
import MainLayout from './layouts/MainLayout';
-import LoginPage from './pages/login';
+const LoginPage = lazy(() => import('./pages/login'));
const HomePage = lazy(() => import('./pages/home'));
const InstancePage = lazy(() => import('./pages/instance'));
const TopicPage = lazy(() => import('./pages/instance/topic'));
@@ -141,7 +141,23 @@ export function LazyRouteOutlet() {
function App() {
return (
<Routes>
- <Route path="/login" element={<LoginPage />} />
+ <Route
+ path="/login"
+ element={
+ <Suspense
+ fallback={
+ <div
+ role="status"
+ style={{ minHeight: '100vh', display: 'grid', placeItems:
'center' }}
+ >
+ <Spin size="large" />
+ </div>
+ }
+ >
+ <LoginPage />
+ </Suspense>
+ }
+ />
<Route element={<AuthGate />}>
<Route path="/" element={<MainLayout />}>
<Route element={<LazyRouteOutlet />}>
diff --git a/web/src/hooks/useApi.ts b/web/src/hooks/useApi.ts
deleted file mode 100644
index 509ff89f..00000000
--- a/web/src/hooks/useApi.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { useState, useCallback } from 'react';
-
-function useApi<T>(apiFn: (...args: unknown[]) => Promise<T>) {
- const [data, setData] = useState<T | null>(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState<Error | null>(null);
-
- const execute = useCallback(
- async (...args: unknown[]) => {
- setLoading(true);
- setError(null);
- try {
- const result = await apiFn(...args);
- setData(result);
- return result;
- } catch (err) {
- setError(err as Error);
- throw err;
- } finally {
- setLoading(false);
- }
- },
- [apiFn],
- );
-
- return { data, loading, error, execute };
-}
-
-export default useApi;
diff --git a/web/src/i18n/LangContext.tsx b/web/src/i18n/LangContext.tsx
index aedf7176..120baef2 100644
--- a/web/src/i18n/LangContext.tsx
+++ b/web/src/i18n/LangContext.tsx
@@ -52,10 +52,15 @@ export const LangProvider = ({ children }: { children:
ReactNode }) => {
return <LangContext.Provider value={{ lang, setLang, t
}}>{children}</LangContext.Provider>;
};
+// The provider component and its consuming hooks intentionally live together;
+// this is a context module, not a component module, so fast-refresh's
+// single-component-export rule does not apply.
+// eslint-disable-next-line react-refresh/only-export-components
export const useLang = () => useContext(LangContext);
/**
* Alias for useLang – provides the same i18n context.
* Kept for backward compatibility with components that import `useLanguage`.
*/
+// eslint-disable-next-line react-refresh/only-export-components
export const useLanguage = useLang;
diff --git a/web/src/pages/instance/consumer.tsx
b/web/src/pages/instance/consumer.tsx
index 7e30eece..f9601a31 100644
--- a/web/src/pages/instance/consumer.tsx
+++ b/web/src/pages/instance/consumer.tsx
@@ -178,6 +178,8 @@ const isConsistentSubscription = (subscription:
SubscriptionEntry): boolean =>
const isInconsistentSubscription = (subscription: SubscriptionEntry): boolean
=>
isInconsistentValue(subscription.consistency);
+// Shared helper exported alongside the page component; fast-refresh rule
waived.
+// eslint-disable-next-line react-refresh/only-export-components
export const diagnosticCacheKey = (instanceId: string, groupName: string) =>
`${instanceId}\u0000${groupName}`;
diff --git a/web/src/pages/studio/GroupManagement.tsx
b/web/src/pages/studio/GroupManagement.tsx
index f3c01ee1..8a2e05e1 100644
--- a/web/src/pages/studio/GroupManagement.tsx
+++ b/web/src/pages/studio/GroupManagement.tsx
@@ -109,6 +109,7 @@ const GroupManagementPage = () => {
// Reset on (re)mount: under StrictMode the previous cleanup has already
// cleared the flag, and without this the remounted load never applies.
mountedRef.current = true;
+ const requestId = listRequestId.current;
const timeoutId = window.setTimeout(() => {
void loadGroups();
});
@@ -116,7 +117,7 @@ const GroupManagementPage = () => {
window.clearTimeout(timeoutId);
mountedRef.current = false;
listRefreshQueued.current = false;
- ++listRequestId.current;
+ listRequestId.current = requestId + 1;
};
}, [loadGroups]);
diff --git a/web/src/pages/studio/Proxy.tsx b/web/src/pages/studio/Proxy.tsx
index 5cd3c850..a2989e64 100644
--- a/web/src/pages/studio/Proxy.tsx
+++ b/web/src/pages/studio/Proxy.tsx
@@ -115,11 +115,12 @@ const ProxyPage: React.FC = () => {
}, [message, t]);
useEffect(() => {
+ const requestId = loadRequestId.current;
// The state updates are performed by the asynchronous Proxy API request,
not by this effect itself.
// eslint-disable-next-line react-hooks/set-state-in-effect
void loadProxyNodes();
return () => {
- ++loadRequestId.current;
+ loadRequestId.current = requestId + 1;
};
}, [loadProxyNodes]);
diff --git a/web/src/stores/clusterStore.test.ts
b/web/src/stores/clusterStore.test.ts
deleted file mode 100644
index 8463c33f..00000000
--- a/web/src/stores/clusterStore.test.ts
+++ /dev/null
@@ -1,128 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { afterEach, describe, expect, it, vi } from 'vitest';
-import type { ClusterInfo } from '../api/cluster';
-import { listClusters } from '../services/clusterService';
-import useClusterStore from './clusterStore';
-
-vi.mock('../services/clusterService', () => ({
- listClusters: vi.fn(),
-}));
-
-const cluster: ClusterInfo = {
- id: 'cluster-prod',
- name: 'rocketmq-prod',
- nsClusterName: 'ns-prod',
- type: 'V5_PROXY_CLUSTER',
- endpoint: '10.101.2.1:9876',
- status: 'healthy',
- version: '5.2.0',
- brokers: [],
- proxies: [],
- nameServers: [],
- config: {
- flushDiskType: 'SYNC_FLUSH',
- autoCreateTopicEnable: false,
- autoCreateSubscriptionGroup: false,
- maxMessageSize: 4194304,
- msgTraceTopicName: 'RMQ_SYS_TRACE_TOPIC4',
- fileReservedTime: 72,
- writeQueueNums: 16,
- readQueueNums: 16,
- brokerPermission: 6,
- deleteWhen: '04',
- },
- topicCount: 256,
- groupCount: 128,
- tpsHistory: [100, 120],
-};
-
-const newerCluster: ClusterInfo = {
- ...cluster,
- id: 'cluster-staging',
- name: 'rocketmq-staging',
-};
-
-function deferred<T>() {
- let resolve!: (value: T) => void;
- let reject!: (reason?: unknown) => void;
- const promise = new Promise<T>((resolvePromise, rejectPromise) => {
- resolve = resolvePromise;
- reject = rejectPromise;
- });
-
- return { promise, resolve, reject };
-}
-
-describe('clusterStore', () => {
- afterEach(() => {
- vi.mocked(listClusters).mockReset();
- useClusterStore.setState({ clusters: [], loading: false });
- });
-
- it('loads clusters from the cluster service', async () => {
- vi.mocked(listClusters).mockResolvedValue([cluster]);
-
- await useClusterStore.getState().fetchClusters();
-
- expect(listClusters).toHaveBeenCalledTimes(1);
- expect(useClusterStore.getState()).toMatchObject({
- clusters: [cluster],
- loading: false,
- });
- });
-
- it('keeps the newest cluster list when overlapping loads finish out of
order', async () => {
- const firstLoad = deferred<ClusterInfo[]>();
- const secondLoad = deferred<ClusterInfo[]>();
- vi.mocked(listClusters)
- .mockReturnValueOnce(firstLoad.promise)
- .mockReturnValueOnce(secondLoad.promise);
-
- const firstFetch = useClusterStore.getState().fetchClusters();
- const secondFetch = useClusterStore.getState().fetchClusters();
-
- secondLoad.resolve([newerCluster]);
- await secondFetch;
-
- expect(useClusterStore.getState()).toMatchObject({
- clusters: [newerCluster],
- loading: false,
- });
-
- firstLoad.resolve([cluster]);
- await firstFetch;
-
- expect(useClusterStore.getState()).toMatchObject({
- clusters: [newerCluster],
- loading: false,
- });
- });
-
- it('resets loading when loading clusters fails', async () => {
- const error = new Error('failed to load clusters');
- vi.mocked(listClusters).mockRejectedValue(error);
-
- await
expect(useClusterStore.getState().fetchClusters()).rejects.toThrow(error);
-
- expect(useClusterStore.getState()).toMatchObject({
- clusters: [],
- loading: false,
- });
- });
-});
diff --git a/web/src/stores/clusterStore.ts b/web/src/stores/clusterStore.ts
deleted file mode 100644
index ea6c349f..00000000
--- a/web/src/stores/clusterStore.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import { create } from 'zustand';
-import { listClusters } from '../services/clusterService';
-import type { ClusterInfo } from '../api/cluster';
-
-interface ClusterState {
- clusters: ClusterInfo[];
- loading: boolean;
- fetchClusters: () => Promise<void>;
-}
-
-let latestFetchRequestId = 0;
-
-const useClusterStore = create<ClusterState>((set) => ({
- clusters: [],
- loading: false,
- fetchClusters: async () => {
- const requestId = ++latestFetchRequestId;
- set({ loading: true });
- try {
- const clusters = await listClusters();
- if (requestId === latestFetchRequestId) {
- set({ clusters });
- }
- } finally {
- if (requestId === latestFetchRequestId) {
- set({ loading: false });
- }
- }
- },
-}));
-
-export default useClusterStore;