From f7e34f16908c6bef0df1919260aa436dc930d03f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gjermund=20H=C3=B8s=C3=B8ien=20Wiggen?= Date: Tue, 9 Jun 2026 13:14:47 +0200 Subject: [PATCH] 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 --- web/src/app/dashboards/[id]/page.tsx | 29 ++++++++- web/src/app/page.tsx | 7 ++- web/src/components/app-shell.tsx | 93 ++++++++++++++++++---------- 3 files changed, 93 insertions(+), 36 deletions(-) diff --git a/web/src/app/dashboards/[id]/page.tsx b/web/src/app/dashboards/[id]/page.tsx index ce679d9..3e74907 100644 --- a/web/src/app/dashboards/[id]/page.tsx +++ b/web/src/app/dashboards/[id]/page.tsx @@ -54,6 +54,7 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string const [views, setViews] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [autoRefresh, setAutoRefresh] = useState(false); // Add widget dialog const [addOpen, setAddOpen] = useState(false); @@ -94,6 +95,23 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string }); }, [fetchDashboard]); + // Auto-refresh: only refresh widget data, not structure + useEffect(() => { + if (!autoRefresh || !dashboard) return; + const interval = setInterval(() => { + for (const widget of widgets) { + getWidgetData(dashboard.id, widget.id).then(({ data: wData }) => { + if (wData) { + setWidgets((prev) => + prev.map((w) => (w.id === widget.id ? { ...w, data: wData } : w)) + ); + } + }); + } + }, 30_000); + return () => clearInterval(interval); + }, [autoRefresh, dashboard?.id]); + const handleAddWidget = async () => { if (!addViewId || !addTitle.trim()) return; setAdding(true); @@ -194,6 +212,15 @@ export default function DashboardPage({ params }: { params: Promise<{ id: string )}
+
) : ( -
+
{widgets.map((widget) => (
{error && ( -
- {error} +
+ {error} +
)} diff --git a/web/src/components/app-shell.tsx b/web/src/components/app-shell.tsx index c63793c..7375da1 100644 --- a/web/src/components/app-shell.tsx +++ b/web/src/components/app-shell.tsx @@ -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([]); const [dashboards, setDashboards] = useState([]); const [currentUserId, setCurrentUserId] = useState(null); + const [expanded, setExpanded] = useState>({ + dashboards: true, + queues: true, + views: true, + }); useEffect(() => { // Find current user and compute view counts @@ -190,41 +196,21 @@ function SidebarNav() { })}
- {queues.length > 0 && ( -
- {!collapsed && ( -
- Queues -
- )} - {queues.map((queue) => { - const active = - pathname === "/" && searchParams.get("queue") === queue.id; - const QueueIcon = () => ( - - ); - return ( - - ); - })} -
- )} - {dashboards.length > 0 && (
{!collapsed && ( -
+
+ )} - {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 && (
{!collapsed && ( -
+
+ )} - {savedViews.map((view) => { + {expanded.views && savedViews.map((view) => { const active = pathname === "/" && searchParams.get("view_id") === view.id; return ( @@ -262,6 +255,40 @@ function SidebarNav() { })}
)} + + {queues.length > 0 && ( +
+ {!collapsed && ( + + )} + {expanded.queues && queues.map((queue) => { + const active = + pathname === "/" && searchParams.get("queue") === queue.id; + const QueueIcon = () => ( + + ); + return ( + + ); + })} +
+ )} ); }