Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
402 changes: 402 additions & 0 deletions web/src/components/dashboard/RouteManagerHubPanel.jsx

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions web/src/components/dashboard/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import ChartsPanel from './ChartsPanel';
import ApiInfoPanel from './ApiInfoPanel';
import AnnouncementsPanel from './AnnouncementsPanel';
import FaqPanel from './FaqPanel';
import RouteManagerHubPanel from './RouteManagerHubPanel';
import UptimePanel from './UptimePanel';
import SearchModal from './modals/SearchModal';

Expand All @@ -51,6 +52,7 @@ import {
getUptimeStatusText,
renderMonitorList,
} from '../../helpers/dashboard';
import { buildRouteManagerHubAvailabilitySignature } from '../../helpers/hubAvailability';

const Dashboard = () => {
// ========== Context ==========
Expand Down Expand Up @@ -132,12 +134,19 @@ const Dashboard = () => {
label: dashboardData.t(info.label),
}),
);
const hubAvailabilitySignature = buildRouteManagerHubAvailabilitySignature(
statusState?.status?.hub_status,
);

// ========== Effects ==========
useEffect(() => {
initChart();
}, []);

useEffect(() => {
void dashboardData.loadHubData();
}, [hubAvailabilitySignature]);
Comment on lines +146 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This effect can race stale hub responses back into state.

Every availability change now starts a fresh loadHubData() call, but web/src/hooks/dashboard/useDashboardData.js:286-390 does not cancel or sequence its multi-request pipeline. If an older request resolves after a later “unavailable” update, it can repopulate stale hub nodes/alerts/summary on the dashboard.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/dashboard/index.jsx` around lines 146 - 148, The effect
calling dashboardData.loadHubData on hubAvailabilitySignature can allow stale
responses to overwrite newer state; update the implementation so requests are
cancellable or sequenced: add an AbortController param or a per-call sequence
token (e.g., a requestId stored on a ref inside the useDashboardData hook) and
have dashboardData.loadHubData accept the signal or capture the requestId and
early-return when aborted or when requestId mismatches; then change the
useEffect that calls dashboardData.loadHubData to create and pass an
AbortController (and call controller.abort() in the cleanup) or increment the
requestId so only the latest pipeline in useDashboardData (lines around
loadHubData and its internal multi-request pipeline) sets state.

⚠️ Potential issue | 🟠 Major

Respect console.hub visibility before fetching or rendering the hub panel.

Line 147 loads hub data and Line 183 renders the panel unconditionally, so disabling SidebarModulesAdmin.console.hub only hides the sidebar entry. The dashboard still exposes the module and its data.

💡 Suggested fix
+import { useSidebar } from '../../hooks/common/useSidebar';
 import { useDashboardData } from '../../hooks/dashboard/useDashboardData';
@@
 const Dashboard = () => {
+  const { isModuleVisible } = useSidebar();
+  const hubModuleVisible = isModuleVisible('console', 'hub');
+
@@
   useEffect(() => {
-    void dashboardData.loadHubData();
-  }, [hubAvailabilitySignature]);
+    if (!hubModuleVisible) {
+      return;
+    }
+    void dashboardData.loadHubData();
+  }, [hubAvailabilitySignature, hubModuleVisible]);
@@
-      <div className='mb-4'>
-        <RouteManagerHubPanel
-          hubStatus={statusState?.status?.hub_status}
-          hubNodes={dashboardData.hubNodes}
-          hubSchedules={dashboardData.hubSchedules}
-          hubTasks={dashboardData.hubTasks}
-          hubAlerts={dashboardData.hubAlerts}
-          hubSummary={dashboardData.hubSummary}
-          hubLoading={dashboardData.hubLoading}
-          hubError={dashboardData.hubError}
-          loadHubData={dashboardData.loadHubData}
-          CARD_PROPS={CARD_PROPS}
-          t={dashboardData.t}
-        />
-      </div>
+      {hubModuleVisible && (
+        <div className='mb-4'>
+          <RouteManagerHubPanel
+            hubStatus={statusState?.status?.hub_status}
+            hubNodes={dashboardData.hubNodes}
+            hubSchedules={dashboardData.hubSchedules}
+            hubTasks={dashboardData.hubTasks}
+            hubAlerts={dashboardData.hubAlerts}
+            hubSummary={dashboardData.hubSummary}
+            hubLoading={dashboardData.hubLoading}
+            hubError={dashboardData.hubError}
+            loadHubData={dashboardData.loadHubData}
+            CARD_PROPS={CARD_PROPS}
+            t={dashboardData.t}
+          />
+        </div>
+      )}

Based on learnings: The sidebar management system introduced in this codebase uses SidebarModulesAdmin configuration to control admin user permissions. Admin access to console.* modules should be governed by this configuration system, not bypassed with hardcoded allowlists.

Also applies to: 182-196

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/dashboard/index.jsx` around lines 146 - 148, The dashboard
currently always calls dashboardData.loadHubData() and always renders the hub
panel regardless of the admin config; wrap both the fetch and the JSX that
renders the hub panel with a check against SidebarModulesAdmin.console.hub (use
the same boolean that controls the sidebar entry) so that
dashboardData.loadHubData() is only invoked inside the useEffect when
SidebarModulesAdmin.console.hub is true (in the effect that depends on
hubAvailabilitySignature) and the hub panel JSX (the block rendering the hub UI
around where HubPanel is returned) is conditional on the same flag.


return (
<div className='h-full'>
<DashboardHeader
Expand Down Expand Up @@ -170,6 +179,22 @@ const Dashboard = () => {
CHART_CONFIG={CHART_CONFIG}
/>

<div className='mb-4'>
<RouteManagerHubPanel
hubStatus={statusState?.status?.hub_status}
hubNodes={dashboardData.hubNodes}
hubSchedules={dashboardData.hubSchedules}
hubTasks={dashboardData.hubTasks}
hubAlerts={dashboardData.hubAlerts}
hubSummary={dashboardData.hubSummary}
hubLoading={dashboardData.hubLoading}
hubError={dashboardData.hubError}
loadHubData={dashboardData.loadHubData}
CARD_PROPS={CARD_PROPS}
t={dashboardData.t}
/>
</div>

{/* API信息和图表面板 */}
<div className='mb-4'>
<div
Expand Down
57 changes: 54 additions & 3 deletions web/src/components/layout/SiderBar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,22 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/

import React, { useEffect, useMemo, useState } from 'react';
import React, { useContext, useEffect, useMemo, useState } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { getLucideIcon } from '../../helpers/render';
import { ChevronLeft } from 'lucide-react';
import { useSidebarCollapsed } from '../../hooks/common/useSidebarCollapsed';
import { useSidebar } from '../../hooks/common/useSidebar';
import { useMinimumLoadingTime } from '../../hooks/common/useMinimumLoadingTime';
import { isAdmin, isRoot, showError } from '../../helpers';
import {
getRouteManagerHubSidebarItems,
isAdmin,
isRoot,
showError,
shouldShowRouteManagerHubEntry,
} from '../../helpers';
import { StatusContext } from '../../context/Status';
import SkeletonWrapper from './components/SkeletonWrapper';

import { Nav, Divider, Button } from '@douyinfe/semi-ui';
Expand Down Expand Up @@ -53,6 +60,7 @@ const routerMap = {

const SiderBar = ({ onNavigate = () => {} }) => {
const { t } = useTranslation();
const [statusState] = useContext(StatusContext);
const [collapsed, toggleCollapsed] = useSidebarCollapsed();
const {
isModuleVisible,
Expand All @@ -67,6 +75,23 @@ const SiderBar = ({ onNavigate = () => {} }) => {
const [openedKeys, setOpenedKeys] = useState([]);
const location = useLocation();
const [routerMapState, setRouterMapState] = useState(routerMap);
const routeManagerHubVisible = shouldShowRouteManagerHubEntry(
statusState?.status?.hub_status,
);
const routeManagerHubSidebarItems = useMemo(
() => getRouteManagerHubSidebarItems(t),
[t],
);
const fullReloadRouteMap = useMemo(
() =>
routeManagerHubVisible
? routeManagerHubSidebarItems.reduce((result, item) => {
result[item.itemKey] = item.to;
return result;
}, {})
: {},
[routeManagerHubSidebarItems, routeManagerHubVisible],
Comment on lines +78 to +93

@coderabbitai coderabbitai Bot Mar 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Gate Hub navigation on reachable, not only configured.

Line 78 uses shouldShowRouteManagerHubEntry(statusState?.status?.hub_status), and that helper only checks configured. In the { configured: true, reachable: false } state, this sidebar still renders full-reload Hub links even though web/src/hooks/dashboard/useDashboardData.js treats the Hub as unavailable and clears its data.

💡 Suggested fix
-  const routeManagerHubVisible = shouldShowRouteManagerHubEntry(
-    statusState?.status?.hub_status,
-  );
+  const hubStatus = statusState?.status?.hub_status;
+  const routeManagerHubVisible = Boolean(
+    hubStatus?.configured && hubStatus?.reachable,
+  );

Also applies to: 133-138

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/layout/SiderBar.jsx` around lines 78 - 93, The sidebar
currently gates Hub links using
shouldShowRouteManagerHubEntry(statusState?.status?.hub_status) which only
checks configured; change the gate to require the Hub be reachable too
(configured && reachable) so links are hidden when hub_status.reachable is
false—either update shouldShowRouteManagerHubEntry to include reachable or
replace the check with statusState?.status?.hub_status?.configured &&
statusState?.status?.hub_status?.reachable; apply the same change for both uses
(the routeManagerHubVisible computation and the other spot that mirrors lines
~133-138) and ensure fullReloadRouteMap is only built when the Hub is both
configured and reachable.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yuheng-888, that's a reasonable UX rationale — preserving the sidebar entry as a recovery path during transient outages is a valid design choice, especially when the upstream reachability check can be flaky. I'll note this intent for future reviews.


✏️ Learnings added
Learnt from: yuheng-888
Repo: QuantumNous/new-api PR: 3485
File: web/src/components/layout/SiderBar.jsx:78-93
Timestamp: 2026-03-28T13:33:28.052Z
Learning: In `web/src/components/layout/SiderBar.jsx`, `routeManagerHubVisible` is intentionally gated only on `hub_status.configured` (via `shouldShowRouteManagerHubEntry`), NOT on `hub_status.reachable`. The sidebar hub entry is kept visible when the hub is configured-but-unreachable so users retain a direct navigation path to `/hub/` for retry, diagnostics, and recovery during transient outages. Do not suggest adding a `reachable` check to this gate.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

);

const workspaceItems = useMemo(() => {
const items = [
Expand Down Expand Up @@ -105,6 +130,12 @@ const SiderBar = ({ onNavigate = () => {} }) => {
className:
localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle',
},
{
text: t('家域中枢'),
itemKey: 'hub',
items: routeManagerHubSidebarItems,
className: routeManagerHubVisible ? '' : 'tableHiddle',
},
];

// 根据配置过滤项目
Expand All @@ -118,6 +149,8 @@ const SiderBar = ({ onNavigate = () => {} }) => {
localStorage.getItem('enable_data_export'),
localStorage.getItem('enable_drawing'),
localStorage.getItem('enable_task'),
routeManagerHubVisible,
routeManagerHubSidebarItems,
t,
isModuleVisible,
]);
Expand Down Expand Up @@ -338,6 +371,8 @@ const SiderBar = ({ onNavigate = () => {} }) => {

// 渲染子菜单项
const renderSubItem = (item) => {
if (item.className === 'tableHiddle') return null;

if (item.items && item.items.length > 0) {
const isSelected = selectedKeys.includes(item.itemKey);
const textColor = isSelected ? SELECTED_COLOR : 'inherit';
Expand All @@ -361,13 +396,16 @@ const SiderBar = ({ onNavigate = () => {} }) => {
}
>
{item.items.map((subItem) => {
if (subItem.className === 'tableHiddle') return null;

const isSubSelected = selectedKeys.includes(subItem.itemKey);
const subTextColor = isSubSelected ? SELECTED_COLOR : 'inherit';

return (
<Nav.Item
key={subItem.itemKey}
itemKey={subItem.itemKey}
className={subItem.className}
text={
<span
className='truncate font-medium text-sm'
Expand Down Expand Up @@ -410,6 +448,19 @@ const SiderBar = ({ onNavigate = () => {} }) => {
hoverStyle='sidebar-nav-item:hover'
selectedStyle='sidebar-nav-item-selected'
renderWrapper={({ itemElement, props }) => {
const fullReloadHref = fullReloadRouteMap[props.itemKey];
if (fullReloadHref) {
return (
<a
style={{ textDecoration: 'none' }}
href={fullReloadHref}
onClick={onNavigate}
>
{itemElement}
</a>
);
}

const to =
routerMapState[props.itemKey] || routerMap[props.itemKey];

Expand Down Expand Up @@ -457,7 +508,7 @@ const SiderBar = ({ onNavigate = () => {} }) => {
{!collapsed && (
<div className='sidebar-group-label'>{t('控制台')}</div>
)}
{workspaceItems.map((item) => renderNavItem(item))}
{workspaceItems.map((item) => renderSubItem(item))}
</div>
</>
)}
Expand Down
Loading