Files
aiops/frontend/src/components/layout/Sidebar.tsx
T
LoganandClaude 76ec9387f0 feat(rdp): 新增 RDP 远程桌面/SMB/账单同步能力 + 清理编译产物
主要变更:
- RDP 远程桌面:新增 backend/pkg/rdp(RDP 引擎 + credssp/display/rle/trace)、
  backend/controller/rdp*(连接/凭证/文件/会话/实时)、frontend RDP 组件与 API
- Windows SMB 文件传输:backend/pkg/winsmb
- 云账单同步:backend/pkg/billingsync + billing_sync/aliyun_billing,前端账单详情
- Agent CLI Kubernetes 工具:kubernetes_action/get、pod_execute/logs
- 服务器导入助手:server_import_helpers、ImportCredentialSettings
- 国际化与组件重构(cloud/deploy/monitor/server/system 等多模块)
- 清理编译产物二进制:移除 backend/server、backend/agent/bin/aiops-agent-*
- .gitignore:新增 backend/agent/ 与 .cache/(避免 334MB go-build 缓存误入库)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-04 01:50:36 +08:00

411 lines
19 KiB
TypeScript

import { useEffect } from 'react';
import { NavLink, useLocation } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { useThemeStore, useMenuStore } from '@/store';
import { usePermission } from '@/hooks/usePermission';
import {
LayoutDashboard,
Server,
Cloud,
Bell,
Monitor,
CheckSquare,
PlayCircle,
Settings,
ChevronDown,
Terminal,
Cpu,
Database,
FileCode,
Shield,
Users,
Network,
Globe,
Key,
Brain,
Activity,
BarChart3,
GitBranch,
Bot,
AlertTriangle,
Container,
Boxes,
Clock,
Rocket,
ShieldCheck,
FileSearch,
HardDrive,
LogIn,
ScrollText,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
interface MenuItem {
id: string;
name: string;
/** i18n 翻译 key(如 "menu.dashboard"),存在时优先使用 t(labelKey) 渲染 */
labelKey?: string;
icon: React.ElementType;
path?: string;
children?: MenuItem[];
/** RBAC 权限码 — 用户需拥有此权限才显示该菜单项;不填=所有人可见 */
permission?: string;
}
const menuItems: MenuItem[] = [
{ id: 'dashboard', name: '仪表盘', labelKey: 'menu.dashboard', icon: LayoutDashboard, path: '/', permission: 'dashboard:view' },
{
id: 'assets', name: '资产管理', labelKey: 'menu.assets', icon: Server, permission: 'server:view',
children: [
{ id: 'servers', name: '服务器管理', labelKey: 'menu.servers', icon: Database, path: '/servers', permission: 'server:view' },
{ id: 'cloud-accounts', name: '多云账号', labelKey: 'menu.cloudAccounts', icon: Key, path: '/cloud-accounts', permission: 'cloud:account' },
{ id: 'domains', name: '域名管理', labelKey: 'menu.domains', icon: Globe, path: '/domains', permission: 'cloud:domain' },
{ id: 'cloud-billing', name: '云账单', labelKey: 'menu.cloudBilling', icon: BarChart3, path: '/cloud-billing', permission: 'cloud:billing' },
],
},
{
id: 'kubernetes', name: 'Kubernetes', labelKey: 'menu.kubernetes', icon: Cloud, permission: 'k8s:cluster',
children: [
{ id: 'k8s-clusters', name: '集群列表', labelKey: 'menu.clusterList', icon: Cloud, path: '/kubernetes', permission: 'k8s:cluster' },
{ id: 'k8s-nodes', name: '节点管理', labelKey: 'menu.nodeManagement', icon: Cpu, path: '/nodes', permission: 'k8s:nodes' },
{
id: 'k8s-workload', name: '工作负载', labelKey: 'menu.workloads', icon: Boxes, permission: 'k8s:workloads',
children: [
{ id: 'k8s-deployments', name: 'Deployments', labelKey: 'menu.deployments', icon: Boxes, path: '/workloads/deployments', permission: 'k8s:workloads' },
{ id: 'k8s-statefulsets', name: 'StatefulSets', labelKey: 'menu.statefulsets', icon: Boxes, path: '/workloads/statefulsets', permission: 'k8s:workloads' },
{ id: 'k8s-daemonsets', name: 'DaemonSets', labelKey: 'menu.daemonsets', icon: Boxes, path: '/workloads/daemonsets', permission: 'k8s:workloads' },
{ id: 'k8s-cronjobs', name: 'CronJobs', labelKey: 'menu.cronjobs', icon: Clock, path: '/workloads/cronjobs', permission: 'k8s:workloads' },
{ id: 'k8s-pods', name: 'Pods', labelKey: 'menu.pods', icon: Container, path: '/workloads/pods', permission: 'k8s:workloads' },
],
},
{
id: 'k8s-network', name: '网络', labelKey: 'menu.network', icon: Network, permission: 'k8s:workloads',
children: [
{ id: 'k8s-services', name: 'Services', labelKey: 'menu.services', icon: Network, path: '/workloads/services', permission: 'k8s:workloads' },
{ id: 'k8s-ingresses', name: 'Ingresses', labelKey: 'menu.ingresses', icon: Globe, path: '/workloads/ingresses', permission: 'k8s:workloads' },
],
},
{
id: 'k8s-config', name: '配置中心', labelKey: 'menu.configCenter', icon: Settings, permission: 'k8s:workloads',
children: [
{ id: 'k8s-configmaps', name: 'ConfigMaps', labelKey: 'menu.configmaps', icon: FileCode, path: '/workloads/configmaps', permission: 'k8s:workloads' },
{ id: 'k8s-secrets', name: 'Secrets', labelKey: 'menu.secrets', icon: Shield, path: '/workloads/secrets', permission: 'k8s:workloads' },
],
},
{ id: 'k8s-storage', name: '存储', labelKey: 'menu.storage', icon: Database, path: '/workloads/pvcs', permission: 'k8s:workloads' },
{ id: 'k8s-events', name: '事件', labelKey: 'menu.events', icon: Bell, path: '/workloads/events', permission: 'k8s:workloads' },
{ id: 'k8s-topology', name: '架构拓扑', labelKey: 'menu.topology', icon: Network, path: '/topology', permission: 'k8s:topology' },
],
},
{
id: 'deploy', name: '应用发布', labelKey: 'menu.deploy', icon: Rocket, permission: 'deploy:pipeline',
children: [
{ id: 'pipelines', name: '流水线', labelKey: 'menu.pipelines', icon: GitBranch, path: '/pipelines', permission: 'deploy:pipeline' },
{ id: 'deploy-credentials', name: '发布配置', labelKey: 'menu.deployConfig', icon: Key, path: '/deploy/configureTools', permission: 'deploy:release' },
{ id: 'deploy-rbac', name: 'Job 权限', labelKey: 'menu.deployRbac', icon: Shield, path: '/deploy/rbac', permission: 'deploy:release' },
],
},
{
id: 'monitor', name: '监控中心', labelKey: 'menu.monitor', icon: Bell, permission: 'monitor:alert',
children: [
{ id: 'grafana', name: '监控大屏', labelKey: 'menu.grafana', icon: Monitor, path: '/dashboards', permission: 'monitor:alert' },
{ id: 'monitor-center', name: '资源监控', labelKey: 'menu.resourceMonitor', icon: Activity, path: '/monitor-center', permission: 'agent:monitor' },
{ id: 'trace', name: '链路追踪', labelKey: 'menu.trace', icon: GitBranch, path: '/trace', permission: 'monitor:alert' },
{ id: 'alert-center', name: '告警中心', labelKey: 'menu.alertCenter', icon: Bell, path: '/alert-center', permission: 'monitor:alert' },
{ id: 'datasources', name: '数据源', labelKey: 'menu.datasources', icon: Database, path: '/datasources', permission: 'monitor:alert' },
],
},
// 日志中心:与监控中心平级,四个子菜单
{
id: 'log-center', name: '日志中心', labelKey: 'menu.logCenter', icon: ScrollText, permission: 'monitor:alert',
children: [
{ id: 'log-loki', name: 'Loki查询', labelKey: 'menu.lokiQuery', icon: FileSearch, path: '/log-center/loki', permission: 'monitor:alert' },
{ id: 'log-es', name: 'ES查询', labelKey: 'menu.esQuery', icon: Database, path: '/log-center/es', permission: 'monitor:alert' },
{ id: 'log-datasources', name: '日志库', labelKey: 'menu.logLibrary', icon: HardDrive, path: '/log-center/datasources', permission: 'monitor:alert' },
{ id: 'log-alerts', name: '日志告警', labelKey: 'menu.logAlerts', icon: AlertTriangle, path: '/log-center/alerts', permission: 'monitor:alert' },
{ id: 'log-rbac', name: '日志权限', labelKey: 'menu.logRbac', icon: Shield, path: '/log-center/rbac', permission: 'monitor:alert' },
],
},
{
id: 'task', name: '作业任务', labelKey: 'menu.automation', icon: PlayCircle, permission: 'auto:cron',
children: [
{ id: 'cronjobs', name: '定时任务', labelKey: 'menu.cronJobs', icon: PlayCircle, path: '/cronjobs', permission: 'auto:cron' },
{ id: 'scripts', name: '脚本库', labelKey: 'menu.scripts', icon: FileCode, path: '/scripts', permission: 'auto:script' },
{ id: 'remote-exec', name: '远程执行', labelKey: 'menu.remoteExec', icon: Terminal, path: '/remote', permission: 'auto:ansible' },
],
},
{
id: 'aiops', name: 'AIOps智能', labelKey: 'menu.aiops', icon: Brain, permission: 'aiops:assistant',
children: [
{ id: 'ai-assistant', name: '智能助手', labelKey: 'menu.aiAssistant', icon: Bot, path: '/ai-assistant', permission: 'aiops:assistant' },
{ id: 'ai-alert-analyzer', name: '告警分析', labelKey: 'menu.aiAlertAnalyzer', icon: AlertTriangle, path: '/ai-alert-analyzer', permission: 'aiops:analysis' },
{ id: 'ai-predictive', name: '预测分析', labelKey: 'menu.aiPredictive', icon: BarChart3, path: '/ai-predictive', permission: 'aiops:forecast' },
{ id: 'ai-agent-cli', name: 'Agent CLI', labelKey: 'menu.aiAgentCli', icon: Terminal, path: '/ai-agent-cli', permission: 'aiops:agent-cli' },
{ id: 'ai-llm-config', name: '大模型设置', labelKey: 'menu.aiLlmConfig', icon: Settings, path: '/ai-llm-config', permission: 'aiops:llm-config' },
],
},
{ id: 'approval', name: '资源申请', labelKey: 'menu.approval', icon: CheckSquare, permission: 'approval:apply',
children: [
{ id: 'approval-resource', name: '资源申请', labelKey: 'menu.approvalResource', icon: Server, path: '/approval/resource', permission: 'approval:apply' },
{ id: 'approval-app', name: '应用申请', labelKey: 'menu.approvalApp', icon: Rocket, path: '/approval/app', permission: 'approval:apply' },
{ id: 'approval-permission', name: '权限审批', labelKey: 'menu.approvalPermission', icon: ShieldCheck, path: '/approval/permission', permission: 'approval:apply' },
],
},
{
id: 'audit', name: '安全审计', labelKey: 'menu.audit', icon: FileSearch, permission: 'audit:terminal',
children: [
{ id: 'audit-terminal', name: '终端审计', labelKey: 'menu.auditTerminal', icon: Terminal, path: '/audit/terminal', permission: 'audit:terminal' },
{ id: 'audit-operations', name: '操作审计', labelKey: 'menu.auditOperations', icon: FileCode, path: '/audit/operations', permission: 'audit:operations' },
{ id: 'audit-login', name: '登录日志', labelKey: 'menu.auditLogin', icon: LogIn, path: '/audit/login-logs', permission: 'audit:login-logs' },
],
},
{
id: 'system', name: '系统管理', labelKey: 'menu.system', icon: Settings, permission: 'system:user',
children: [
{ id: 'users', name: '用户管理', labelKey: 'menu.users', icon: Users, path: '/users', permission: 'system:user' },
{ id: 'roles', name: '角色权限', labelKey: 'menu.roles', icon: Shield, path: '/roles', permission: 'system:role' },
{ id: 'credentials', name: '凭证配置', labelKey: 'menu.credentials', icon: Shield, path: '/credentials', permission: 'system:user' },
{ id: 'approval-workflow', name: '审批流程', labelKey: 'menu.approvalWorkflow', icon: GitBranch, path: '/approval-workflow', permission: 'system:role' },
{ id: 'settings', name: '系统设置', labelKey: 'menu.settings', icon: Settings, path: '/settings', permission: 'system:user' },
],
},
];
/**
* MenuItemComponent - 侧边栏菜单项(支持多级嵌套)
*
* 优化点:
* 1. 使用 Tooltip 在折叠状态下显示菜单名称
* 2. 动画过渡平滑
* 3. 活动状态判断更精确
* 4. 子菜单展开/收起动画
*/
function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number }) {
const location = useLocation();
const { t } = useTranslation();
const { openMenus, toggleMenu, activeMenu, setActiveMenu } = useMenuStore();
const { sidebarCollapsed } = useThemeStore();
const hasChildren = item.children && item.children.length > 0;
const isOpen = openMenus.includes(item.id);
// 优先使用 i18n 翻译 key;无 labelKey 则回退到 name
const displayName = item.labelKey ? t(item.labelKey, { defaultValue: item.name }) : item.name;
// Determine if this menu item is active
const isActive = item.path
? location.pathname === item.path || location.pathname.startsWith(item.path + '/')
: (item.id === 'kubernetes' && location.pathname.startsWith('/kubernetes')) ||
(item.id === 'k8s-workload' && location.pathname.startsWith('/workloads')) ||
(item.id === 'k8s-network' && (location.pathname.includes('/services') || location.pathname.includes('/ingresses'))) ||
(item.id === 'k8s-config' && (location.pathname.includes('/configmaps') || location.pathname.includes('/secrets'))) ||
(item.id === 'log-center' && location.pathname.startsWith('/log-center')) ||
activeMenu === item.id;
const isActiveParent = hasChildren && item.children?.some((child) => {
if (child.path) {
return location.pathname === child.path || location.pathname.startsWith(child.path + '/');
}
return false;
});
const handleClick = () => {
if (hasChildren) {
toggleMenu(item.id);
} else if (item.path) {
setActiveMenu(item.id);
}
};
// Collapsed mode (only top-level)
if (sidebarCollapsed && depth === 0) {
return (
<TooltipProvider delayDuration={100}>
<Tooltip>
<TooltipTrigger asChild>
<div className="relative">
{hasChildren ? (
<Button
variant={isActive || isActiveParent ? 'secondary' : 'ghost'}
size="icon"
className={cn(
'w-10 h-10 mx-auto mb-1 transition-all duration-200',
(isActive || isActiveParent)
? 'bg-[var(--dg-blue)]/15 text-[var(--dg-blue)]'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)}
onClick={handleClick}
>
<item.icon className="h-5 w-5" />
</Button>
) : (
<Button
variant={isActive ? 'secondary' : 'ghost'}
size="icon"
className={cn(
'w-10 h-10 mx-auto mb-1 transition-all duration-200',
isActive
? 'bg-[var(--dg-blue)]/15 text-[var(--dg-blue)]'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)}
asChild
>
<NavLink to={item.path || '#'} onClick={handleClick}>
<item.icon className="h-5 w-5" />
</NavLink>
</Button>
)}
</div>
</TooltipTrigger>
<TooltipContent side="right" className="bg-[var(--dg-bg-secondary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]">
{displayName}
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
return (
<div className={cn('mb-0.5', depth > 0 && 'ml-2.5')}>
{hasChildren ? (
<>
<Button
variant={isActive || isActiveParent ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-between h-8 px-2.5 text-xs font-normal transition-all duration-200',
(isActive || isActiveParent)
? 'bg-[var(--dg-blue)]/10 text-[var(--dg-blue)] hover:bg-[var(--dg-blue)]/15'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)}
onClick={handleClick}
>
<span className="flex items-center gap-2">
<item.icon className={cn('h-3.5 w-3.5', (isActive || isActiveParent) && 'text-[var(--dg-blue)]')} />
<span className="truncate">{displayName}</span>
</span>
<ChevronDown
className={cn(
'h-3 w-3 text-[var(--dg-text-muted)] transition-transform duration-200',
!isOpen && '-rotate-90',
)}
/>
</Button>
{/* Submenu with animation */}
<div
className={cn(
'overflow-hidden transition-all duration-200 ease-in-out',
isOpen ? 'max-h-[800px] opacity-100 mt-0.5' : 'max-h-0 opacity-0',
)}
>
<div className="space-y-0.5 border-l border-[var(--dg-border)] ml-4 pl-1.5">
{item.children?.map((child) => (
<MenuItemComponent key={child.id} item={child} depth={depth + 1} />
))}
</div>
</div>
</>
) : (
<NavLink
to={item.path || '#'}
onClick={handleClick}
className={({ isActive: navActive }) =>
cn(
'flex items-center gap-2 h-8 px-2.5 text-xs rounded-md transition-all duration-200',
navActive || isActive
? 'bg-[var(--dg-blue)]/10 text-[var(--dg-blue)] font-medium'
: 'text-[var(--dg-text-secondary)] hover:text-[var(--dg-text-primary)] hover:bg-[var(--dg-bg-hover)]',
)
}
>
<item.icon className="h-3.5 w-3.5 flex-shrink-0" />
<span className="truncate">{displayName}</span>
</NavLink>
)}
</div>
);
}
/**
* Sidebar - 优化后的侧边栏导航
*
* 优化点:
* 1. 使用 CSS 变量控制宽度,统一协调
* 2. 添加 Tooltip 支持(折叠状态)
* 3. 子菜单展开/收起动画
* 4. Logo 区域更精致
* 5. 底部添加版本信息
*/
export function Sidebar() {
const { t } = useTranslation();
const { sidebarCollapsed, toggleSidebar } = useThemeStore();
const { hasPermission } = usePermission();
// Listen for keyboard shortcut
useEffect(() => {
const handler = () => toggleSidebar();
window.addEventListener('toggle-sidebar', handler);
return () => window.removeEventListener('toggle-sidebar', handler);
}, [toggleSidebar]);
// F3: 按权限过滤菜单(递归)
// 父级菜单:自身权限通过 且 至少有一个子菜单可见
const filterMenu = (items: MenuItem[]): MenuItem[] => {
return items
.filter((item) => !item.permission || hasPermission(item.permission))
.map((item) => {
if (!item.children) return item;
const visibleChildren = filterMenu(item.children);
return { ...item, children: visibleChildren };
})
.filter((item) => !item.children || item.children.length > 0);
};
const visibleMenuItems = filterMenu(menuItems);
return (
<aside
className={cn(
'fixed left-0 top-0 z-40 h-screen bg-[var(--dg-bg-secondary)] border-r border-[var(--dg-border)]',
'flex flex-col transition-[width] duration-300 ease-in-out',
)}
style={{ width: 'var(--sidebar-width)' }}
>
{/* Logo Area */}
<div className="flex items-center h-14 px-3 border-b border-[var(--dg-border)] flex-shrink-0">
<div className={cn('flex items-center gap-2.5 flex-1 min-w-0', sidebarCollapsed && 'justify-center')}>
<div className="dg-icon-box w-8 h-8 rounded-lg flex-shrink-0">
<Cloud className="h-5 w-5" />
</div>
{!sidebarCollapsed && (
<span className="font-bold text-[var(--dg-text-primary)] text-sm tracking-tight truncate">
{t('common.appName')}
</span>
)}
</div>
{/* 折叠按钮统一放在 Header,避免与顶栏重复 // Logan.Li */}
</div>
{/* Menu Items */}
<ScrollArea className="flex-1 py-2 px-1.5">
<nav className="space-y-0.5">
{visibleMenuItems.map((item) => (
<MenuItemComponent key={item.id} item={item} />
))}
</nav>
</ScrollArea>
{/* Footer */}
{!sidebarCollapsed && (
<div className="px-3 py-2 border-t border-[var(--dg-border)] flex-shrink-0">
<div className="text-[10px] text-[var(--dg-text-muted)] text-center">
AIOps v1.0.0
</div>
</div>
)}
</aside>
);
}