feat: PTY kubectl终端 + 侧边栏对齐修复 + 构建缓存清理

**kubectl WebShell 修复:**
- 引入 github.com/creack/pty 库实现真正的伪终端
- 替换 bash --noediting + TERM=dumb 方案为 PTY-backed bash
- 支持 xterm-256color、窗口自适应、readline 交互
- 修复键盘输入无响应问题

**侧边栏优化:**
- 独立菜单项高度从 py-1.5 改为 h-8,与父菜单对齐
- 独立菜单图标从 h-3 w-3 改为 h-3.5 w-3.5,统一 14px

**构建优化:**
- run.sh docker up 增加 docker image prune -f 清理悬空镜像
- 每次部署自动清理 builder cache + dangling images

**认证修复(历史):**
- Token 存储:Zustand + localStorage 双写,http.ts 双源 fallback
- YamlViewer/DeploymentDetail token 来源统一为 Zustand store
This commit is contained in:
attacker committed 2026-06-19 11:22:45 +00:00
1 parent cac1414917
commit a3cd0d3910
29 files changed
+2954 -180

No files matched your search

+67 -133
View File
@@ -3,37 +3,12 @@ import { NavLink, useLocation } from 'react-router-dom';
import { cn } from '@/lib/utils';
import { useThemeStore, useMenuStore } from '@/store';
import {
LayoutDashboard,
Server,
Cloud,
Rocket,
Bell,
CheckSquare,
PlayCircle,
Settings,
ChevronDown,
ChevronRight,
Terminal,
Cpu,
Database,
FileCode,
Shield,
Users,
Menu,
X,
Network,
CloudCog,
Globe,
MapPin,
Key,
Brain,
FileTerminal,
Activity,
BarChart3,
GitBranch,
Monitor,
Bot,
AlertTriangle,
LayoutDashboard, Server, Cloud, Rocket, Bell, Monitor,
CheckSquare, PlayCircle, Settings, ChevronDown, ChevronRight,
Terminal, Cpu, Database, FileCode, Shield, Users,
Menu, Network, CloudCog, Globe, MapPin, Key,
Brain, FileTerminal, Activity, BarChart3, GitBranch,
Bot, AlertTriangle, Container, Boxes, Clock,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ScrollArea } from '@/components/ui/scroll-area';
@@ -49,9 +24,7 @@ interface MenuItem {
const menuItems: MenuItem[] = [
{ id: 'dashboard', name: '仪表盘', icon: LayoutDashboard, path: '/' },
{
id: 'assets',
name: '资产管理',
icon: Server,
id: 'assets', name: '资产管理', icon: Server,
children: [
{ id: 'servers', name: '服务器管理', icon: Database, path: '/servers' },
{ id: 'cloud-accounts', name: '云账号管理', icon: Key, path: '/cloud-accounts' },
@@ -62,29 +35,50 @@ const menuItems: MenuItem[] = [
],
},
{
id: 'kubernetes',
name: 'Kubernetes',
icon: Cloud,
id: 'kubernetes', name: 'Kubernetes', icon: Cloud,
children: [
{ id: 'clusters', name: '集群列表', icon: Cloud, path: '/clusters' },
{ id: 'nodes', name: '节点管理', icon: Cpu, path: '/nodes' },
{ id: 'workloads', name: '工作负载', icon: Database, path: '/workloads' },
{ id: 'topology', name: '架构拓扑', icon: Network, path: '/topology' },
{ id: 'k8s-clusters', name: '集群列表', icon: Cloud, path: '/kubernetes' },
{ id: 'k8s-nodes', name: '节点管理', icon: Cpu, path: '/nodes' },
{
id: 'k8s-workload', name: '工作负载', icon: Boxes,
children: [
{ id: 'k8s-deployments', name: 'Deployments', icon: Boxes, path: '/workloads/deployments' },
{ id: 'k8s-statefulsets', name: 'StatefulSets', icon: Boxes, path: '/workloads/statefulsets' },
{ id: 'k8s-daemonsets', name: 'DaemonSets', icon: Boxes, path: '/workloads/daemonsets' },
{ id: 'k8s-cronjobs', name: 'CronJobs', icon: Clock, path: '/workloads/cronjobs' },
{ id: 'k8s-pods', name: 'Pods', icon: Container, path: '/workloads/pods' },
],
},
{
id: 'k8s-network', name: '网络', icon: Network,
children: [
{ id: 'k8s-services', name: 'Services', icon: Network, path: '/workloads/services' },
{ id: 'k8s-ingresses', name: 'Ingresses', icon: Globe, path: '/workloads/ingresses' },
],
},
{
id: 'k8s-config', name: '配置中心', icon: Settings,
children: [
{ id: 'k8s-configmaps', name: 'ConfigMaps', icon: FileCode, path: '/workloads/configmaps' },
{ id: 'k8s-secrets', name: 'Secrets', icon: Shield, path: '/workloads/secrets' },
],
},
{ id: 'k8s-storage', name: '存储', icon: Database, path: '/workloads/pvcs' },
{ id: 'k8s-metrics', name: '资源用量', icon: Activity, path: '/workloads/metrics' },
{ id: 'k8s-events', name: '事件', icon: Bell, path: '/workloads/events' },
{ id: 'k8s-capacity', name: '容量分析', icon: BarChart3, path: '/workloads/capacity' },
{ id: 'k8s-topology', name: '架构拓扑', icon: Network, path: '/topology' },
],
},
{
id: 'deploy',
name: '应用发布',
icon: Rocket,
id: 'deploy', name: '应用发布', icon: Rocket,
children: [
{ id: 'pipelines', name: '流水线管理', icon: GitBranch, path: '/pipelines' },
{ id: 'releases', name: '发布管理', icon: Rocket, path: '/releases' },
],
},
{
id: 'monitor',
name: '监控中心',
icon: Bell,
id: 'monitor', name: '监控中心', icon: Bell,
children: [
{ id: 'grafana', name: '监控大屏', icon: Monitor, path: '/dashboards' },
{ id: 'monitor-center', name: '资源监控', icon: Activity, path: '/monitor-center' },
@@ -94,9 +88,7 @@ const menuItems: MenuItem[] = [
],
},
{
id: 'automation',
name: '自动化运维',
icon: PlayCircle,
id: 'automation', name: '自动化运维', icon: PlayCircle,
children: [
{ id: 'cronjobs', name: '定时任务', icon: PlayCircle, path: '/cronjobs' },
{ id: 'scripts', name: '脚本库', icon: FileCode, path: '/scripts' },
@@ -104,9 +96,7 @@ const menuItems: MenuItem[] = [
],
},
{
id: 'aiops',
name: 'AIOps智能',
icon: Brain,
id: 'aiops', name: 'AIOps智能', icon: Brain,
children: [
{ id: 'ai-assistant', name: '智能助手', icon: Bot, path: '/ai-assistant' },
{ id: 'ai-alert-analyzer', name: '告警分析', icon: AlertTriangle, path: '/ai-alert-analyzer' },
@@ -115,9 +105,7 @@ const menuItems: MenuItem[] = [
},
{ id: 'approval', name: '资源申请', icon: CheckSquare, path: '/approval' },
{
id: 'system',
name: '系统管理',
icon: Settings,
id: 'system', name: '系统管理', icon: Settings,
children: [
{ id: 'users', name: '用户管理', icon: Users, path: '/users' },
{ id: 'roles', name: '角色权限', icon: Shield, path: '/roles' },
@@ -135,7 +123,11 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
const { sidebarCollapsed } = useThemeStore();
const hasChildren = item.children && item.children.length > 0;
const isOpen = openMenus.includes(item.id);
const isActive = item.path === location.pathname || activeMenu === item.id;
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')) ||
activeMenu === item.id;
const handleClick = () => {
if (hasChildren) {
@@ -151,38 +143,22 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
<Button
variant={isActive ? 'secondary' : 'ghost'}
size="icon"
className={cn(
'w-10 h-10 mx-auto mb-1',
isActive && 'bg-blue-600/20 text-blue-400'
)}
className={cn('w-10 h-10 mx-auto mb-1', isActive && 'bg-blue-600/20 text-blue-400')}
asChild={!hasChildren}
>
{hasChildren ? (
<span onClick={handleClick}>
<item.icon className="h-5 w-5" />
</span>
<span onClick={handleClick}><item.icon className="h-5 w-5" /></span>
) : (
<NavLink to={item.path || '#'} onClick={handleClick}>
<item.icon className="h-5 w-5" />
</NavLink>
<NavLink to={item.path || '#'} onClick={handleClick}><item.icon className="h-5 w-5" /></NavLink>
)}
</Button>
{/* Tooltip */}
<div className="absolute left-full top-1/2 -translate-y-1/2 ml-2 px-3 py-1.5 bg-slate-800 text-slate-100 text-sm rounded-md opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all whitespace-nowrap z-50 border border-slate-700">
{item.name}
{hasChildren && (
<div className="absolute left-0 top-0 mt-2 ml-2 w-48 bg-slate-800 rounded-md border border-slate-700 shadow-lg py-1">
{item.children?.map((child) => (
<NavLink
key={child.id}
to={child.path || '#'}
className={({ isActive }) =>
cn(
'block px-4 py-2 text-sm hover:bg-slate-700 transition-colors',
isActive ? 'text-blue-400 bg-blue-600/10' : 'text-slate-300'
)
}
>
<NavLink key={child.id} to={child.path || '#'}
className={({ isActive }) => cn('block px-4 py-2 text-sm hover:bg-slate-700 transition-colors', isActive ? 'text-blue-400 bg-blue-600/10' : 'text-slate-300')}>
{child.name}
</NavLink>
))}
@@ -194,46 +170,24 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
}
return (
<div className={cn('mb-0.5', depth > 0 && 'ml-4')}>
<div className={cn('mb-0.5', depth > 0 && 'ml-3')}>
{hasChildren ? (
<>
<Button
variant={isActive ? 'secondary' : 'ghost'}
className={cn(
'w-full justify-between h-9 px-3 text-sm font-normal',
isActive ? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30' : 'text-slate-300 hover:text-slate-100 hover:bg-slate-800'
)}
className={cn('w-full justify-between h-8 px-2.5 text-xs font-normal', isActive ? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30' : 'text-slate-300 hover:text-slate-100 hover:bg-slate-800')}
onClick={handleClick}
>
<span className="flex items-center gap-2">
<item.icon className="h-4 w-4" />
<span className="flex items-center gap-1.5">
<item.icon className="h-3.5 w-3.5" />
<span>{item.name}</span>
</span>
{isOpen ? (
<ChevronDown className="h-4 w-4 text-slate-500" />
) : (
<ChevronRight className="h-4 w-4 text-slate-500" />
)}
{isOpen ? <ChevronDown className="h-3 w-3 text-slate-500" /> : <ChevronRight className="h-3 w-3 text-slate-500" />}
</Button>
{isOpen && (
<div className="mt-0.5 space-y-0.5">
{item.children?.map((child) => (
<NavLink
key={child.id}
to={child.path || '#'}
onClick={() => setActiveMenu(child.id)}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors',
isActive
? 'bg-blue-600/20 text-blue-400'
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800'
)
}
>
<child.icon className="h-3.5 w-3.5" />
<span>{child.name}</span>
</NavLink>
<MenuItemComponent key={child.id} item={child} depth={depth + 1} />
))}
</div>
)}
@@ -243,15 +197,11 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
to={item.path || '#'}
onClick={handleClick}
className={({ isActive }) =>
cn(
'flex items-center gap-2 px-3 py-2 text-sm rounded-md transition-colors',
isActive
? 'bg-blue-600/20 text-blue-400'
: 'text-slate-300 hover:text-slate-100 hover:bg-slate-800'
)
cn('flex items-center gap-1.5 h-8 px-2.5 text-xs rounded-md transition-colors',
isActive ? 'bg-blue-600/20 text-blue-400' : 'text-slate-400 hover:text-slate-100 hover:bg-slate-800')
}
>
<item.icon className="h-4 w-4" />
<item.icon className="h-3.5 w-3.5" />
<span>{item.name}</span>
</NavLink>
)}
@@ -261,15 +211,8 @@ function MenuItemComponent({ item, depth = 0 }: { item: MenuItem; depth?: number
export function Sidebar() {
const { sidebarCollapsed, toggleSidebar } = useThemeStore();
return (
<aside
className={cn(
'fixed left-0 top-0 z-40 h-screen bg-slate-900 border-r border-slate-800 transition-all duration-300',
sidebarCollapsed ? 'w-16' : 'w-64'
)}
>
{/* Logo */}
<aside className={cn('fixed left-0 top-0 z-40 h-screen bg-slate-900 border-r border-slate-800 transition-all duration-300', sidebarCollapsed ? 'w-16' : 'w-64')}>
<div className="flex items-center justify-between h-14 px-4 border-b border-slate-800">
{!sidebarCollapsed && (
<div className="flex items-center gap-2">
@@ -285,23 +228,14 @@ export function Sidebar() {
</div>
)}
{!sidebarCollapsed && (
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-slate-100"
onClick={toggleSidebar}
>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-slate-100" onClick={toggleSidebar}>
<Menu className="h-4 w-4" />
</Button>
)}
</div>
{/* Menu */}
<ScrollArea className="h-[calc(100vh-3.5rem)]">
<div className={cn('p-2', sidebarCollapsed && 'px-1')}>
{menuItems.map((item) => (
<MenuItemComponent key={item.id} item={item} />
))}
<div className={cn('p-1.5', sidebarCollapsed && 'px-1')}>
{menuItems.map((item) => <MenuItemComponent key={item.id} item={item} />)}
</div>
</ScrollArea>
</aside>