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:
1 parent
cac1414917
commit
a3cd0d3910
29 files changed
+2954
-180
No files matched your search
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user