feat: dashboard auto-refresh, collapsible sidebar, error retry

- Dashboard: auto-refresh toggle (30s interval, spins when live)
- Dashboard: responsive grid (6 cols mobile, 12 cols desktop)
- Sidebar: Dashboards, Saved views, Queues sections now collapsible
  with chevron toggle
- Error banner: added Retry button next to error message

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 13:14:47 +02:00
parent 6263ce1332
commit f7e34f1690
3 changed files with 93 additions and 36 deletions

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, Suspense, createContext, useContext } from "react"
import { usePathname, useSearchParams } from "next/navigation";
import Link from "next/link";
import {
ChevronRightIcon,
LayoutGridIcon,
UserIcon,
InboxIcon,
@@ -89,6 +90,11 @@ function SidebarNav() {
const [savedViews, setSavedViews] = useState<SavedView[]>([]);
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
const [currentUserId, setCurrentUserId] = useState<string | null>(null);
const [expanded, setExpanded] = useState<Record<string, boolean>>({
dashboards: true,
queues: true,
views: true,
});
useEffect(() => {
// Find current user and compute view counts
@@ -190,41 +196,21 @@ function SidebarNav() {
})}
</div>
{queues.length > 0 && (
<div>
{!collapsed && (
<div className="px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase">
Queues
</div>
)}
{queues.map((queue) => {
const active =
pathname === "/" && searchParams.get("queue") === queue.id;
const QueueIcon = () => (
<span className="w-2 h-2 rounded-full bg-sidebar-primary flex-shrink-0 shadow-[0_0_0_3px_color-mix(in_oklch,var(--sidebar-primary)_18%,transparent)]" />
);
return (
<SidebarNavItem
key={queue.id}
href={`/?queue=${queue.id}`}
icon={QueueIcon}
label={queue.name}
count={queue.count}
active={active}
/>
);
})}
</div>
)}
{dashboards.length > 0 && (
<div className="mt-4">
{!collapsed && (
<div className="px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase">
<button
type="button"
onClick={() => setExpanded((e) => ({ ...e, dashboards: !e.dashboards }))}
className="flex w-full items-center gap-1 px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase hover:text-sidebar-foreground/70"
>
<ChevronRightIcon
className={cn("h-3 w-3 transition-transform", expanded.dashboards && "rotate-90")}
/>
Dashboards
</div>
</button>
)}
{dashboards.map((dash) => {
{expanded.dashboards && dashboards.map((dash) => {
const active =
pathname.startsWith("/dashboards/") && pathname.endsWith(dash.id);
return (
@@ -243,11 +229,18 @@ function SidebarNav() {
{savedViews.length > 0 && (
<div className="mt-4">
{!collapsed && (
<div className="px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase">
<button
type="button"
onClick={() => setExpanded((e) => ({ ...e, views: !e.views }))}
className="flex w-full items-center gap-1 px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase hover:text-sidebar-foreground/70"
>
<ChevronRightIcon
className={cn("h-3 w-3 transition-transform", expanded.views && "rotate-90")}
/>
Saved views
</div>
</button>
)}
{savedViews.map((view) => {
{expanded.views && savedViews.map((view) => {
const active =
pathname === "/" && searchParams.get("view_id") === view.id;
return (
@@ -262,6 +255,40 @@ function SidebarNav() {
})}
</div>
)}
{queues.length > 0 && (
<div className="mt-4">
{!collapsed && (
<button
type="button"
onClick={() => setExpanded((e) => ({ ...e, queues: !e.queues }))}
className="flex w-full items-center gap-1 px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase hover:text-sidebar-foreground/70"
>
<ChevronRightIcon
className={cn("h-3 w-3 transition-transform", expanded.queues && "rotate-90")}
/>
Queues
</button>
)}
{expanded.queues && queues.map((queue) => {
const active =
pathname === "/" && searchParams.get("queue") === queue.id;
const QueueIcon = () => (
<span className="w-2 h-2 rounded-full bg-sidebar-primary flex-shrink-0 shadow-[0_0_0_3px_color-mix(in_oklch,var(--sidebar-primary)_18%,transparent)]" />
);
return (
<SidebarNavItem
key={queue.id}
href={`/?queue=${queue.id}`}
icon={QueueIcon}
label={queue.name}
count={queue.count}
active={active}
/>
);
})}
</div>
)}
</>
);
}