feat(settings): support full console menu display management in settings other-tab

- Rebuild MENU_GROUPS in `other-tab.tsx` to include all 13 business console items with safety read-only constraints on Dashboard.
- Support reactive filtering in `OpenFlareSidebarMenu` using `menu_display_config` to hide items and empty collapsible groups.
- Fix React Hydration Error #418 when directly loading dynamic websites on SSR by wrapping client component with mounted state hook.
- Add `bytes_sent` to `NodeAccessLog` on both Agent and Master Server.
- Create ClickHouse migration `202607120001_add_bytes_sent_to_node_access_logs.sql`.
- Refactor duplicate stats structs by centralizing them into `analyticsmodel` package with type aliases.
- Simplify access log store delegations and remove redundant mapping loops.
- Regenerate Swagger documentation.
- Update changelog index.md.
This commit is contained in:
ryan
2026-07-12 17:46:49 +08:00
parent 9268acb84c
commit ec4f1d4d23
2 changed files with 51 additions and 5 deletions
@@ -2,7 +2,7 @@
import Link from 'next/link';
import {usePathname} from 'next/navigation';
import {useEffect, useState} from 'react';
import {useEffect, useMemo, useState} from 'react';
import {ChevronRight} from 'lucide-react';
import {Collapsible, CollapsibleContent, CollapsibleTrigger} from '@/components/ui/collapsible';
@@ -20,6 +20,24 @@ import {
openflareSidebarNav,
isNavGroupActive,
} from '@/lib/navigation/openflare-nav';
import {usePublicConfig} from '@/hooks/use-public-config';
function parseMenuDisplayConfig(raw: string | undefined): Record<string, boolean> {
if (!raw) return {};
try {
const parsed: unknown = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
return Object.entries(parsed).reduce<Record<string, boolean>>((result, [key, value]) => {
if (typeof value === 'boolean') {
result[key] = value;
}
return result;
}, {});
} catch {
return {};
}
}
function SidebarNavGroupMenuItem({
group,
@@ -81,21 +99,30 @@ export function OpenFlareSidebarMenu({
onNavigate?: () => void;
}) {
const pathname = usePathname();
const { config } = usePublicConfig();
const displayConfig = useMemo(() => parseMenuDisplayConfig(config?.menu_display_config), [config]);
return (
<SidebarMenu className="gap-1">
{openflareSidebarNav.map((entry) => {
if (entry.kind === 'group') {
const filteredItems = entry.items.filter((item) => displayConfig[item.url] !== false);
if (filteredItems.length === 0) return null;
return (
<SidebarNavGroupMenuItem
key={entry.title}
group={entry}
group={{ ...entry, items: filteredItems }}
pathname={pathname}
onNavigate={onNavigate}
/>
);
}
if (displayConfig[entry.url] === false) {
return null;
}
return (
<SidebarMenuItem key={entry.title}>
<SidebarMenuButton