Skip to content

Commit f6d133c

Browse files
committed
feat: introduce project workspace filtering and unified dashboard console drawer
- Added activeProject workspace context state and actions to useAppStore. - Redesigned SideNav to group items logically and integrated a dynamic project dropdown selector. - Wired workspace project filtering into Dashboard containers and Compose groups. - Implemented collapsible, combined Live Console Drawer with custom tags, level filters, clear toggles, auto-scroll, and ⌘L keydown shortcut.
1 parent 07579c4 commit f6d133c

3 files changed

Lines changed: 396 additions & 54 deletions

File tree

packages/desktop/src/components/layout/SideNav.tsx

Lines changed: 114 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import { useMemo } from "react";
12
import { NavLink } from "react-router-dom";
3+
import { useContainers } from "../../hooks/useQueries";
4+
import { useAppStore } from "../../store/useAppStore";
25

36
/* ── SVG Icons (Lucide-sourced, inline for zero dependency) ──────────────── */
47

@@ -63,22 +66,54 @@ const SettingsIcon = () => (
6366

6467
/* ── Navigation Definition ───────────────────────────────────────────────── */
6568

66-
const NAV = [
67-
{ to: "/", label: "Dashboard", icon: DashboardIcon, shortcut: "⌘1" },
68-
{ to: "/environments",label: "Environments",icon: EnvIcon, shortcut: "⌘3" },
69-
{ to: "/images", label: "Images", icon: ImagesIcon, shortcut: "⌘6" },
70-
{ to: "/logs", label: "Logs", icon: LogsIcon, shortcut: "⌘2" },
71-
{ to: "/network", label: "Network", icon: NetworkIcon, shortcut: "⌘4" },
72-
{ to: "/volumes", label: "Volumes", icon: VolumesIcon, shortcut: "⌘7" },
73-
{ to: "/settings", label: "Settings", icon: SettingsIcon, shortcut: "⌘5" },
69+
const SECTIONS = [
70+
{
71+
title: "Workspace",
72+
items: [
73+
{ to: "/", label: "Dashboard", icon: DashboardIcon },
74+
{ to: "/logs", label: "Logs", icon: LogsIcon },
75+
],
76+
},
77+
{
78+
title: "Development",
79+
items: [
80+
{ to: "/environments", label: "Environments", icon: EnvIcon },
81+
],
82+
},
83+
{
84+
title: "Infrastructure",
85+
items: [
86+
{ to: "/images", label: "Images", icon: ImagesIcon },
87+
{ to: "/volumes", label: "Volumes", icon: VolumesIcon },
88+
{ to: "/network", label: "Network", icon: NetworkIcon },
89+
],
90+
},
91+
{
92+
title: "System",
93+
items: [
94+
{ to: "/settings", label: "Settings", icon: SettingsIcon },
95+
],
96+
},
7497
];
7598

7699
/* ── Component ───────────────────────────────────────────────────────────── */
77100

78101
export function SideNav() {
102+
const { data: containers } = useContainers();
103+
const { activeProject, setActiveProject } = useAppStore();
104+
105+
const projects = useMemo(() => {
106+
if (!containers) return [];
107+
const set = new Set<string>();
108+
containers.forEach((c) => {
109+
if (c.compose_project) set.add(c.compose_project);
110+
});
111+
return Array.from(set).sort();
112+
}, [containers]);
113+
79114
return (
80115
<nav
81-
className="w-[210px] bg-bg-deep border-r border-border flex flex-col shrink-0"
116+
className="w-[210px] bg-bg-deep border-r border-border flex flex-col shrink-0 select-none"
82117
aria-label="Main navigation"
83118
>
84119
{/* Logo / Wordmark */}
@@ -88,56 +123,86 @@ export function SideNav() {
88123
style={{ boxShadow: "0 0 8px var(--green)" }}
89124
aria-hidden="true"
90125
/>
91-
<span
92-
className="font-mono text-[14px] font-bold text-green tracking-tight select-none"
93-
>
126+
<span className="font-mono text-[14px] font-bold text-green tracking-tight select-none">
94127
DevOpsLocal
95128
</span>
96129
</div>
97130

98-
{/* Nav Items */}
99-
<div className="flex flex-col py-2 flex-1">
100-
{NAV.map(({ to, label, icon: Icon }) => (
101-
<NavLink
102-
key={to}
103-
to={to}
104-
end={to === "/"}
105-
aria-current={undefined /* set by NavLink's isActive */}
106-
className={({ isActive }: { isActive: boolean }) =>
107-
[
108-
"group flex items-center gap-2.5 px-3.5 py-2 mx-2 my-0.5 rounded-lg text-[13px] font-medium transition-all duration-150 no-underline relative",
109-
isActive
110-
? "text-green bg-green/10 shadow-[inset_0_0_0_1px_rgba(var(--color-green)/0.2)]"
111-
: "text-text-3 hover:text-text-2 hover:bg-surface-2",
112-
].join(" ")
113-
}
114-
>
115-
{({ isActive }: { isActive: boolean }) => (
116-
<>
117-
{/* Active left accent bar */}
118-
{isActive && (
119-
<span
120-
className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 rounded-r bg-green"
121-
aria-hidden="true"
122-
/>
131+
{/* Dynamic Project Workspace Selector */}
132+
<div className="px-3.5 py-3 border-b border-border-light flex flex-col gap-1.5 bg-bg-deep/50">
133+
<label className="text-[9px] font-mono font-bold uppercase tracking-wider text-text-3 select-none">
134+
Project Workspace
135+
</label>
136+
<select
137+
value={activeProject}
138+
onChange={(e) => setActiveProject(e.target.value)}
139+
className="w-full bg-surface border border-border rounded px-2.5 py-1.5 text-[11px] text-text font-mono outline-none focus:border-green focus:ring-1 focus:ring-green/30 transition-all cursor-pointer hover:bg-surface-2"
140+
aria-label="Select active project workspace"
141+
>
142+
<option value="all">🌐 All Projects</option>
143+
{projects.map((p) => (
144+
<option key={p} value={p}>
145+
📦 {p}
146+
</option>
147+
))}
148+
</select>
149+
</div>
150+
151+
{/* Nav Items Grouped by Section */}
152+
<div className="flex flex-col py-3 flex-1 overflow-y-auto gap-4">
153+
{SECTIONS.map((section) => (
154+
<div key={section.title} className="flex flex-col gap-0.5">
155+
<span className="px-4 text-[9px] font-mono font-bold uppercase tracking-wider text-text-3/65 mb-1 select-none">
156+
{section.title}
157+
</span>
158+
{section.items.map(({ to, label, icon: Icon }) => (
159+
<NavLink
160+
key={to}
161+
to={to}
162+
end={to === "/"}
163+
className={({ isActive }: { isActive: boolean }) =>
164+
[
165+
"group flex items-center gap-2.5 px-3.5 py-2 mx-2 my-0.5 rounded-lg text-[13px] font-medium transition-all duration-150 no-underline relative",
166+
isActive
167+
? "text-green bg-green/10 shadow-[inset_0_0_0_1px_rgba(var(--color-green)/0.2)] font-semibold"
168+
: "text-text-3 hover:text-text-2 hover:bg-surface-2",
169+
].join(" ")
170+
}
171+
>
172+
{({ isActive }: { isActive: boolean }) => (
173+
<>
174+
{/* Active left accent bar */}
175+
{isActive && (
176+
<span
177+
className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 rounded-r bg-green"
178+
aria-hidden="true"
179+
/>
180+
)}
181+
<span
182+
className={`shrink-0 transition-colors ${
183+
isActive ? "text-green" : "text-text-3 group-hover:text-text-2"
184+
}`}
185+
aria-hidden="true"
186+
>
187+
<Icon />
188+
</span>
189+
<span className="truncate">{label}</span>
190+
</>
123191
)}
124-
<span
125-
className={`shrink-0 transition-colors ${isActive ? "text-green" : "text-text-3 group-hover:text-text-2"}`}
126-
aria-hidden="true"
127-
>
128-
<Icon />
129-
</span>
130-
<span className="truncate">{label}</span>
131-
</>
132-
)}
133-
</NavLink>
192+
</NavLink>
193+
))}
194+
</div>
134195
))}
135196
</div>
136197

137198
{/* Footer hint */}
138-
<div className="px-4 py-3 border-t border-border-light">
199+
<div className="px-4 py-3 border-t border-border-light bg-bg-deep">
139200
<p className="text-[10px] font-mono text-text-3 m-0">
140-
Press <kbd className="bg-surface-3 border border-border px-1 rounded text-[9px]">?</kbd> for shortcuts
201+
Press{" "}
202+
<kbd className="bg-surface-3 border border-border px-1 rounded text-[9px]">
203+
?
204+
</kbd>{" "}
205+
for shortcuts
141206
</p>
142207
</div>
143208
</nav>

0 commit comments

Comments
 (0)