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;

Reply via email to