-
Notifications
You must be signed in to change notification settings - Fork 1
feat: dashboard polish — skeletons, error boundaries, responsive cards #72
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,8 @@ import { Badge } from "@/components/ui/badge"; | |||||||||||||
| import { Agent, Alert, AlertDetails, CostData, AgentStatus, AlertSeverity, Session, Project, Profile, AnalyticsData, SpendData, CostLimits } from "@/lib/types"; | ||||||||||||||
| import { getAgents, getAlerts, getAlertDetails, getCosts, pauseAgent, resumeAgent, acknowledgeAlert, acknowledgeAllAlerts, getSessions, getProjects, getProfiles, getVersion, getAnalytics, getSpend, setCostLimits, isUsingMockData } from "@/lib/api"; | ||||||||||||||
| import { ClaWatchLogo, ClaWatchIcon } from "@/components/clawatch-logo"; | ||||||||||||||
| import { ErrorBoundary } from "@/components/ui/error-boundary"; | ||||||||||||||
| import { StatsOverviewSkeleton, AgentsTabSkeleton, SessionsTabSkeleton, AnalyticsTabSkeleton } from "@/components/ui/dashboard-skeletons"; | ||||||||||||||
| import { AgentsTab } from "./components/AgentsTab"; | ||||||||||||||
| import { SessionsTab } from "./components/SessionsTab"; | ||||||||||||||
| import { AnalyticsTab } from "./components/AnalyticsTab"; | ||||||||||||||
|
|
@@ -477,10 +479,18 @@ function DashboardContent() { | |||||||||||||
|
|
||||||||||||||
| if (loading) { | ||||||||||||||
| return ( | ||||||||||||||
| <div className="min-h-screen bg-background flex items-center justify-center"> | ||||||||||||||
| <div className="flex items-center gap-3 text-muted-foreground"> | ||||||||||||||
| <div className="size-5 border-2 border-emerald-500 border-t-transparent rounded-full animate-spin" /> | ||||||||||||||
| Loading dashboard... | ||||||||||||||
| <div className="min-h-screen bg-background text-foreground"> | ||||||||||||||
| <nav className="border-b border-border/50 bg-background/80 backdrop-blur-sm sticky top-0 z-50"> | ||||||||||||||
| <div className="max-w-7xl mx-auto px-6 h-14 flex items-center justify-between"> | ||||||||||||||
| <div className="flex items-center gap-6"> | ||||||||||||||
| <Link href="/" className="flex items-center gap-2"><ClaWatchIcon /><ClaWatchLogo size="md" /></Link> | ||||||||||||||
| <span className="text-sm text-muted-foreground">Dashboard</span> | ||||||||||||||
| </div> | ||||||||||||||
| </div> | ||||||||||||||
| </nav> | ||||||||||||||
| <div className="max-w-7xl mx-auto px-6 py-6 space-y-6"> | ||||||||||||||
| <StatsOverviewSkeleton /> | ||||||||||||||
| <AgentsTabSkeleton /> | ||||||||||||||
|
||||||||||||||
| <AgentsTabSkeleton /> | |
| {tab === "sessions" | |
| ? <SessionsTabSkeleton /> | |
| : tab === "analytics" | |
| ? <AnalyticsTabSkeleton /> | |
| : <AgentsTabSkeleton />} |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import { Card, CardContent, CardHeader } from "@/components/ui/card" | ||
| import { Skeleton } from "@/components/ui/skeleton" | ||
|
|
||
| export function StatsOverviewSkeleton() { | ||
| return ( | ||
| <div className="grid grid-cols-2 lg:grid-cols-5 gap-4"> | ||
| {Array.from({ length: 5 }).map((_, i) => ( | ||
| <Card key={i}> | ||
| <CardHeader className="pb-2"> | ||
| <Skeleton className="h-4 w-24" /> | ||
| </CardHeader> | ||
| <CardContent> | ||
| <Skeleton className="h-9 w-20 mb-2" /> | ||
| <Skeleton className="h-3 w-16" /> | ||
| </CardContent> | ||
| </Card> | ||
| ))} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export function AgentsTabSkeleton() { | ||
| return ( | ||
| <div className="space-y-6"> | ||
| <div className="flex items-center justify-between mb-4"> | ||
| <Skeleton className="h-6 w-32" /> | ||
| <Skeleton className="h-5 w-36" /> | ||
| </div> | ||
| <div className="grid gap-3"> | ||
| {Array.from({ length: 3 }).map((_, i) => ( | ||
| <div | ||
| key={i} | ||
| className="rounded-xl border border-border/50 bg-card p-4 flex flex-col sm:flex-row items-start sm:items-center gap-4" | ||
| > | ||
| <div className="flex items-center gap-3 min-w-0"> | ||
| <Skeleton className="size-2.5 rounded-full shrink-0" /> | ||
| <div className="space-y-1.5"> | ||
| <Skeleton className="h-4 w-28" /> | ||
| <Skeleton className="h-3 w-20" /> | ||
| </div> | ||
| </div> | ||
| <Skeleton className="h-5 w-16 rounded-full" /> | ||
| <div className="flex items-center gap-6 sm:ml-auto"> | ||
| <div className="space-y-1"> | ||
| <Skeleton className="h-4 w-12" /> | ||
| <Skeleton className="h-3 w-8" /> | ||
| </div> | ||
| <div className="space-y-1"> | ||
| <Skeleton className="h-4 w-12" /> | ||
| <Skeleton className="h-3 w-8" /> | ||
| </div> | ||
| <div className="space-y-1"> | ||
| <Skeleton className="h-4 w-8" /> | ||
| <Skeleton className="h-3 w-10" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export function SessionsTabSkeleton() { | ||
| return ( | ||
| <div className="space-y-4"> | ||
| <div className="flex items-center gap-2 mb-4"> | ||
| {Array.from({ length: 4 }).map((_, i) => ( | ||
| <Skeleton key={i} className="h-8 w-16 rounded-md" /> | ||
| ))} | ||
| </div> | ||
| {Array.from({ length: 5 }).map((_, i) => ( | ||
| <div key={i} className="rounded-xl border border-border/50 bg-card p-4 space-y-2"> | ||
| <div className="flex items-center justify-between"> | ||
| <Skeleton className="h-4 w-48" /> | ||
| <Skeleton className="h-4 w-20" /> | ||
| </div> | ||
| <div className="flex items-center gap-4"> | ||
| <Skeleton className="h-3 w-24" /> | ||
| <Skeleton className="h-3 w-16" /> | ||
| <Skeleton className="h-3 w-20" /> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| ) | ||
| } | ||
|
|
||
| export function AnalyticsTabSkeleton() { | ||
| return ( | ||
| <div className="space-y-6"> | ||
| <div className="flex items-center justify-between"> | ||
| <Skeleton className="h-6 w-40" /> | ||
| <div className="flex gap-2"> | ||
| {Array.from({ length: 5 }).map((_, i) => ( | ||
| <Skeleton key={i} className="h-8 w-14 rounded-md" /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| <Card> | ||
| <CardContent className="pt-4"> | ||
| <Skeleton className="h-64 w-full rounded-lg" /> | ||
| </CardContent> | ||
| </Card> | ||
| <div className="grid lg:grid-cols-2 gap-6"> | ||
| <Card> | ||
| <CardContent className="pt-4 space-y-3"> | ||
| {Array.from({ length: 4 }).map((_, i) => ( | ||
| <div key={i} className="flex items-center justify-between"> | ||
| <Skeleton className="h-4 w-24" /> | ||
| <Skeleton className="h-2 w-32 rounded-full" /> | ||
| </div> | ||
| ))} | ||
| </CardContent> | ||
| </Card> | ||
| <Card> | ||
| <CardContent className="pt-4 space-y-3"> | ||
| {Array.from({ length: 4 }).map((_, i) => ( | ||
| <div key={i} className="flex items-center justify-between"> | ||
| <Skeleton className="h-4 w-24" /> | ||
| <Skeleton className="h-2 w-32 rounded-full" /> | ||
| </div> | ||
| ))} | ||
| </CardContent> | ||
| </Card> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||
| "use client" | ||||||||
|
|
||||||||
| import { Component, type ReactNode } from "react" | ||||||||
|
|
||||||||
| interface ErrorBoundaryProps { | ||||||||
| children: ReactNode | ||||||||
| fallback?: ReactNode | ||||||||
| onReset?: () => void | ||||||||
| } | ||||||||
|
|
||||||||
| interface ErrorBoundaryState { | ||||||||
| hasError: boolean | ||||||||
| error: Error | null | ||||||||
| } | ||||||||
|
|
||||||||
| export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> { | ||||||||
| constructor(props: ErrorBoundaryProps) { | ||||||||
| super(props) | ||||||||
| this.state = { hasError: false, error: null } | ||||||||
| } | ||||||||
|
|
||||||||
| static getDerivedStateFromError(error: Error): ErrorBoundaryState { | ||||||||
| return { hasError: true, error } | ||||||||
| } | ||||||||
|
|
||||||||
| reset = () => { | ||||||||
| this.setState({ hasError: false, error: null }) | ||||||||
| this.props.onReset?.() | ||||||||
| } | ||||||||
|
|
||||||||
| render() { | ||||||||
| if (this.state.hasError) { | ||||||||
| if (this.props.fallback) return this.props.fallback | ||||||||
|
|
||||||||
| return ( | ||||||||
| <div className="rounded-xl border border-red-500/20 bg-red-500/5 p-6 text-center"> | ||||||||
| <div className="text-2xl mb-2">💥</div> | ||||||||
| <div className="text-sm font-medium text-red-400 mb-1"> | ||||||||
| Something went wrong | ||||||||
| </div> | ||||||||
| <div className="text-xs text-muted-foreground mb-4 max-w-md mx-auto break-words"> | ||||||||
| {this.state.error?.message || "An unexpected error occurred"} | ||||||||
| </div> | ||||||||
| <button | ||||||||
|
||||||||
| <button | |
| <button | |
| type="button" |
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,12 @@ | ||||||||||||||||
| import { cn } from "@/lib/utils" | ||||||||||||||||
|
|
||||||||||||||||
| function Skeleton({ className, ...props }: React.ComponentProps<"div">) { | ||||||||||||||||
|
Comment on lines
+1
to
+3
|
||||||||||||||||
| import { cn } from "@/lib/utils" | |
| function Skeleton({ className, ...props }: React.ComponentProps<"div">) { | |
| import type { ComponentProps } from "react" | |
| import { cn } from "@/lib/utils" | |
| function Skeleton({ className, ...props }: ComponentProps<"div">) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused imports:
SessionsTabSkeletonandAnalyticsTabSkeletonare imported here but never referenced in this file. This will trip Next/ESLintno-unused-varsduring CI. Remove them or use them in the loading UI.