feat: make insight sidebar collapsible (#279)

This commit is contained in:
shenlan 2025-09-21 20:42:00 +08:00 committed by GitHub
parent d51678730f
commit a27c4e52df
2 changed files with 208 additions and 92 deletions

View File

@ -1,5 +1,7 @@
'use client'
import { BarChart3, BellRing, Compass, Layers, Sparkles, type LucideIcon, PanelLeftClose, PanelLeftOpen, EyeOff } from 'lucide-react'
import { QueryLanguage, TopologyMode } from '../../insight/store/urlState'
interface SidebarProps {
@ -8,15 +10,18 @@ interface SidebarProps {
onSelectSection: (section: string) => void
onTopologyChange: (mode: TopologyMode) => void
onToggleLanguage: (language: QueryLanguage) => void
onToggleCollapse: () => void
onHide: () => void
activeSection: string
collapsed: boolean
}
const sections = [
{ id: 'topology', label: 'Topology' },
{ id: 'explore', label: 'Explore' },
{ id: 'visualize', label: 'Visualize' },
{ id: 'slo', label: 'SLO & Alerts' },
{ id: 'ai', label: 'AI Assistant' }
const sections: { id: string; label: string; icon: LucideIcon }[] = [
{ id: 'topology', label: 'Topology', icon: Layers },
{ id: 'explore', label: 'Explore', icon: Compass },
{ id: 'visualize', label: 'Visualize', icon: BarChart3 },
{ id: 'slo', label: 'SLO & Alerts', icon: BellRing },
{ id: 'ai', label: 'AI Assistant', icon: Sparkles }
]
const topologyOptions: { id: TopologyMode; label: string; hint: string }[] = [
@ -37,91 +42,184 @@ export function Sidebar({
activeSection,
onSelectSection,
onTopologyChange,
onToggleLanguage
onToggleLanguage,
onToggleCollapse,
onHide,
collapsed
}: SidebarProps) {
return (
<aside className="w-full bg-slate-900/70 backdrop-blur lg:w-72 xl:w-80 border-r border-slate-800 px-4 py-6 space-y-7">
<div className="space-y-2">
<h1 className="text-lg font-semibold text-slate-100">Insight Workbench</h1>
<p className="text-sm text-slate-400">
Navigate topology, run cross-domain queries and keep SLOs on track.
</p>
</div>
<div className="space-y-3 rounded-2xl border border-slate-800 bg-slate-950/60 p-4">
<label className="flex flex-col gap-1 text-xs uppercase tracking-wide text-slate-400">
Topology mode
<div className="relative">
<select
value={topologyMode}
onChange={event => onTopologyChange(event.target.value as TopologyMode)}
className="w-full appearance-none rounded-xl border border-slate-800 bg-slate-900/80 px-3 py-2 text-sm text-slate-200"
>
{topologyOptions.map(option => (
<option key={option.id} value={option.id} className="bg-slate-900 text-slate-200">
{option.label} · {option.hint}
</option>
))}
</select>
<span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">⌄</span>
<aside
className={`border-r border-slate-800 bg-slate-900/70 px-3 py-6 backdrop-blur transition-all duration-200 ${
collapsed ? 'w-20 space-y-6' : 'w-full space-y-7 lg:w-72 xl:w-80'
}`}
>
<div className={`flex items-start justify-between ${collapsed ? 'flex-col items-center gap-4' : ''}`}>
{!collapsed && (
<div className="space-y-2">
<h1 className="text-lg font-semibold text-slate-100">Insight Workbench</h1>
<p className="text-sm text-slate-400">
Navigate topology, run cross-domain queries and keep SLOs on track.
</p>
</div>
</label>
<p className="text-xs text-slate-400">
Choose the focus for the topology map and SLO templates.
</p>
</div>
<div className="rounded-2xl border border-slate-800 bg-slate-950/60 p-4">
<p className="text-xs uppercase tracking-wide text-slate-400">Query domains</p>
<p className="mt-1 text-xs text-slate-500">Select one or more languages to open stacked explorers.</p>
<div className="mt-3 space-y-2">
{languageOptions.map(option => {
const active = activeLanguages.includes(option.id)
return (
<button
key={option.id}
onClick={() => onToggleLanguage(option.id)}
className={`flex w-full items-center justify-between rounded-xl border px-3 py-2 text-left text-sm transition ${
active
? 'border-emerald-500/70 bg-emerald-500/10 text-emerald-200'
: 'border-slate-800 bg-slate-900/60 text-slate-300 hover:border-slate-700'
}`}
>
<span className="font-medium">{option.label}</span>
<span className="text-xs text-slate-400">{option.description}</span>
</button>
)
})}
)}
<div className={`flex items-center gap-2 ${collapsed ? '' : 'ml-2'}`}>
<button
type="button"
onClick={onToggleCollapse}
className="flex h-9 w-9 items-center justify-center rounded-xl border border-slate-800 bg-slate-900/80 text-slate-300 transition hover:border-slate-700 hover:text-slate-100"
aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{collapsed ? <PanelLeftOpen className="h-4 w-4" /> : <PanelLeftClose className="h-4 w-4" />}
</button>
<button
type="button"
onClick={onHide}
className="flex h-9 w-9 items-center justify-center rounded-xl border border-slate-800 bg-slate-900/80 text-slate-300 transition hover:border-red-500/60 hover:text-red-300"
aria-label="Hide sidebar"
>
<EyeOff className="h-4 w-4" />
</button>
</div>
</div>
<nav className="space-y-1">
{sections.map(section => (
<button
key={section.id}
onClick={() => onSelectSection(section.id)}
className={`w-full text-left px-3 py-2 rounded-xl transition-colors ${
activeSection === section.id
? 'bg-slate-800 text-slate-100 shadow-inner shadow-slate-800/60'
: 'text-slate-300 hover:bg-slate-800/60'
}`}
>
{section.label}
</button>
))}
{!collapsed && (
<div className="space-y-3 rounded-2xl border border-slate-800 bg-slate-950/60 p-4">
<label className="flex flex-col gap-1 text-xs uppercase tracking-wide text-slate-400">
Topology mode
<div className="relative">
<select
value={topologyMode}
onChange={event => onTopologyChange(event.target.value as TopologyMode)}
className="w-full appearance-none rounded-xl border border-slate-800 bg-slate-900/80 px-3 py-2 text-sm text-slate-200"
>
{topologyOptions.map(option => (
<option key={option.id} value={option.id} className="bg-slate-900 text-slate-200">
{option.label} · {option.hint}
</option>
))}
</select>
<span className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2 text-xs text-slate-500">⌄</span>
</div>
</label>
<p className="text-xs text-slate-400">
Choose the focus for the topology map and SLO templates.
</p>
</div>
)}
<div
className={`rounded-2xl border border-slate-800 bg-slate-950/60 ${
collapsed ? 'p-3' : 'p-4'
}`}
>
{!collapsed ? (
<>
<p className="text-xs uppercase tracking-wide text-slate-400">Query domains</p>
<p className="mt-1 text-xs text-slate-500">Select one or more languages to open stacked explorers.</p>
<div className="mt-3 space-y-2">
{languageOptions.map(option => {
const active = activeLanguages.includes(option.id)
return (
<button
key={option.id}
onClick={() => onToggleLanguage(option.id)}
className={`flex w-full items-center justify-between rounded-xl border px-3 py-2 text-left text-sm transition ${
active
? 'border-emerald-500/70 bg-emerald-500/10 text-emerald-200'
: 'border-slate-800 bg-slate-900/60 text-slate-300 hover:border-slate-700'
}`}
>
<span className="font-medium">{option.label}</span>
<span className="text-xs text-slate-400">{option.description}</span>
</button>
)
})}
</div>
</>
) : (
<div className="flex flex-col items-center gap-3">
{languageOptions.map(option => {
const active = activeLanguages.includes(option.id)
return (
<button
key={option.id}
onClick={() => onToggleLanguage(option.id)}
className={`flex h-11 w-11 items-center justify-center rounded-xl border text-sm font-semibold transition ${
active
? 'border-emerald-500/70 bg-emerald-500/10 text-emerald-200'
: 'border-slate-800 bg-slate-900/60 text-slate-300 hover:border-slate-700'
}`}
title={option.label}
>
{option.label.substring(0, 2)}
</button>
)
})}
</div>
)}
</div>
<nav className={`space-y-1 ${collapsed ? 'flex flex-col items-center gap-2' : ''}`}>
{sections.map(section => {
const active = activeSection === section.id
const Icon = section.icon
return (
<button
key={section.id}
onClick={() => onSelectSection(section.id)}
className={`w-full rounded-xl transition ${
collapsed
? 'flex flex-col items-center gap-2 px-2 py-3'
: 'flex items-center gap-3 px-3 py-2 text-left'
} ${
active
? 'bg-slate-800 text-slate-100 shadow-inner shadow-slate-800/60'
: 'text-slate-300 hover:bg-slate-800/60'
}`}
title={section.label}
>
<span
className={`flex h-10 w-10 items-center justify-center rounded-xl border ${
active ? 'border-slate-700 bg-slate-800 text-slate-100' : 'border-slate-800 bg-slate-900 text-slate-400'
}`}
>
<Icon className="h-5 w-5" />
</span>
{!collapsed && <span className="font-medium">{section.label}</span>}
</button>
)
})}
</nav>
<div className="rounded-2xl bg-gradient-to-br from-slate-800/80 to-slate-900 p-4 shadow-inner border border-slate-800">
<p className="text-xs uppercase tracking-wide text-slate-400 mb-2">Active explorers</p>
<ul className="space-y-1 text-sm text-slate-300">
{activeLanguages.map(language => (
<li key={language} className="flex items-center justify-between">
<span>{languageLabels[language]}</span>
<span className="text-xs text-slate-500">QL</span>
</li>
))}
{activeLanguages.length === 0 && <li className="text-xs text-slate-500">No languages selected.</li>}
</ul>
<div
className={`rounded-2xl border border-slate-800 bg-gradient-to-br from-slate-800/80 to-slate-900 shadow-inner ${
collapsed ? 'p-3' : 'p-4'
}`}
>
{!collapsed ? (
<>
<p className="mb-2 text-xs uppercase tracking-wide text-slate-400">Active explorers</p>
<ul className="space-y-1 text-sm text-slate-300">
{activeLanguages.map(language => (
<li key={language} className="flex items-center justify-between">
<span>{languageLabels[language]}</span>
<span className="text-xs text-slate-500">QL</span>
</li>
))}
{activeLanguages.length === 0 && <li className="text-xs text-slate-500">No languages selected.</li>}
</ul>
</>
) : (
<div className="flex flex-col items-center gap-2">
<p className="text-xs uppercase tracking-wide text-slate-400">Active</p>
<div className="flex flex-col items-center gap-1 text-[10px] text-slate-300">
{activeLanguages.map(language => (
<span key={language}>{languageLabels[language]}</span>
))}
{activeLanguages.length === 0 && <span className="text-slate-500">None</span>}
</div>
</div>
)}
</div>
</aside>
)

View File

@ -1,6 +1,7 @@
'use client'
import { useCallback, useState } from 'react'
import { PanelLeftOpen } from 'lucide-react'
import { Sidebar } from '../components/insight/layout/Sidebar'
import { TopBar } from '../components/insight/layout/TopBar'
import { TopologyStrip } from '../components/insight/topology/TopologyStrip'
@ -25,6 +26,8 @@ export default function InsightWorkbench() {
logql: [],
traceql: []
})
const [sidebarCollapsed, setSidebarCollapsed] = useState(false)
const [sidebarHidden, setSidebarHidden] = useState(false)
const handleSelectSection = useCallback((section: string) => {
setActiveSection(section)
@ -64,16 +67,31 @@ export default function InsightWorkbench() {
)
return (
<div className="min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
<div className="relative min-h-screen bg-gradient-to-br from-slate-950 via-slate-900 to-slate-950 text-slate-100">
{sidebarHidden && (
<button
type="button"
onClick={() => setSidebarHidden(false)}
className="fixed left-4 top-4 z-20 flex items-center gap-2 rounded-full border border-slate-800 bg-slate-900/80 px-4 py-2 text-sm text-slate-200 shadow-lg backdrop-blur transition hover:border-slate-700 hover:text-slate-100"
>
<PanelLeftOpen className="h-4 w-4" />
Show menu
</button>
)}
<div className="mx-auto flex min-h-screen max-w-7xl flex-col lg:flex-row">
<Sidebar
topologyMode={state.topologyMode}
activeLanguages={state.activeLanguages}
activeSection={activeSection}
onSelectSection={handleSelectSection}
onTopologyChange={mode => updateState({ topologyMode: mode })}
onToggleLanguage={toggleLanguage}
/>
{!sidebarHidden && (
<Sidebar
topologyMode={state.topologyMode}
activeLanguages={state.activeLanguages}
activeSection={activeSection}
onSelectSection={handleSelectSection}
onTopologyChange={mode => updateState({ topologyMode: mode })}
onToggleLanguage={toggleLanguage}
onToggleCollapse={() => setSidebarCollapsed(prev => !prev)}
onHide={() => setSidebarHidden(true)}
collapsed={sidebarCollapsed}
/>
)}
<main className="flex-1 px-4 py-8 lg:px-8">
<div className="grid gap-8 lg:grid-cols-[minmax(0,2.4fr)_minmax(0,1fr)]">
<div className="space-y-6">