Files
ocdp-go/frontend/src/shared/components/layout/SidebarLayout/SidebarNav.tsx
Ivan087 28ecb2e636 feat: scale instances, --reuse-values, values diff, UI redesign, hover animations
Backend (Phase 1):
- Add ScaleInstance endpoint (POST /clusters/{id}/instances/{id}/scale)
- Add GetInstanceValuesDiff endpoint (GET .../values-diff)
- Enable ReuseValues=true in Helm Upgrade for --reuse-values behavior
- Add GetValues/GetChartDefaultValues to HelmClient interface
- Add ScaleInstanceRequest/Response and InstanceValuesDiffResponse DTOs

Frontend (Phase 2):
- InstanceCard: +/- scale buttons with loading spinner
- ModifyModal: values diff view (current vs defaults), Use Defaults button
- ArtifactBrowserPage: collapsible sidebar, compact tag grid, search filter
- TagCard: "LATEST" badge, compact layout, responsive design
- InstanceCard: compact 3-column layout, fewer scrolls needed
- InstancesManagementPage: 3-column grid, compact view
- Global hover-lift and hover-glow CSS utilities
- SidebarNav: subtle hover transition on links
2026-05-13 11:51:24 +08:00

132 lines
3.9 KiB
TypeScript

import React, { useState } from "react";
import { LayoutDashboard, ChevronDown, ChevronRight, X } from "lucide-react";
export interface NavItem {
key: string;
label: string;
icon?: React.ReactNode;
active?: boolean;
onClick?: () => void;
children?: NavItem[];
}
interface SidebarNavProps {
items?: NavItem[];
isOpen?: boolean;
onClose?: () => void;
}
export default function SidebarNav({ items = [] as NavItem[], isOpen = true, onClose }: SidebarNavProps) {
const [expandedKeys, setExpandedKeys] = useState<Set<string>>(new Set(["setup"]));
const toggleExpand = (key: string) => {
setExpandedKeys((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
};
const handleItemClick = (item: NavItem, hasChildren: boolean) => {
if (hasChildren) {
toggleExpand(item.key);
} else {
if (item.onClick) {
item.onClick();
}
// 移动端点击后关闭侧边栏
if (onClose) {
onClose();
}
}
};
const renderNavItem = (item: NavItem, level = 0) => {
const hasChildren = Boolean(item.children && item.children.length > 0);
const isExpanded = expandedKeys.has(item.key);
return (
<div key={item.key}>
<button
onClick={() => handleItemClick(item, hasChildren)}
className={`w-full text-left flex items-center gap-2 px-3 py-2 rounded-xl text-sm font-medium transition-colors duration-150 hover:bg-blue-50 dark:hover:bg-blue-900/20 ${
item.active
? "bg-blue-50 text-blue-700 border border-blue-200 shadow-sm"
: "text-slate-600 hover:text-slate-950"
}`}
style={{ paddingLeft: `${12 + level * 16}px` }}
>
{item.icon}
<span className="flex-1">{item.label}</span>
{hasChildren && (
isExpanded ? (
<ChevronDown className="w-4 h-4" />
) : (
<ChevronRight className="w-4 h-4" />
)
)}
</button>
{hasChildren && isExpanded && (
<div className="mt-1 space-y-1">
{item.children!.map((child) => renderNavItem(child, level + 1))}
</div>
)}
</div>
);
};
return (
<>
{/* 移动端遮罩层 */}
{isOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 md:hidden"
onClick={onClose}
/>
)}
{/* 侧边栏 */}
<aside
className={`
fixed md:static inset-y-0 left-0 z-50 md:z-0
flex flex-col w-64 sm:w-72 md:w-60 xl:w-64 bg-white/95 backdrop-blur-xl border-r border-slate-200 shadow-soft
transform transition-transform duration-300 ease-in-out
${isOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0"}
`}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-4 border-b border-slate-200">
<div className="flex items-center gap-2">
<LayoutDashboard className="w-5 h-5 text-blue-600" />
<span className="text-sm font-semibold text-slate-700 tracking-wide">Operations</span>
</div>
{/* 移动端关闭按钮 */}
{onClose && (
<button
onClick={onClose}
className="md:hidden p-1.5 rounded-lg hover:bg-slate-100 text-slate-500 hover:text-slate-900 transition"
aria-label="关闭菜单"
>
<X className="w-5 h-5" />
</button>
)}
</div>
{/* Navigation */}
<nav className="flex-1 p-3 space-y-1 overflow-y-auto">
{items.map((item) => renderNavItem(item))}
</nav>
{/* Footer */}
<div className="p-3 text-xs text-muted border-t border-slate-200">
OCDP · {new Date().getFullYear()}
</div>
</aside>
</>
);
}