diff --git a/frontend/src/app/dashboard/components/AgentsTab.tsx b/frontend/src/app/dashboard/components/AgentsTab.tsx index 59f47f0..682d671 100644 --- a/frontend/src/app/dashboard/components/AgentsTab.tsx +++ b/frontend/src/app/dashboard/components/AgentsTab.tsx @@ -162,15 +162,15 @@ export function AgentsTab({ {agents.map((agent) => { const sc = statusConfig[agent.status] || statusConfig.idle; return ( -
-
- -
-
{agent.name}
-
{agent.host}
+
+
+ +
+
{agent.name}
+
{agent.host}
-
+
{sc.label} {agent.overLimit && agent.limit != null && (() => { const spend = agent.limitType === "daily" ? (agent.todaySpend ?? 0) : (agent.mtdSpend ?? 0); @@ -178,31 +178,31 @@ export function AgentsTab({ return ⚠️ ${over.toFixed(2)} over; })()}
-
-
+
+
${agent.costUsd.toFixed(2)}
cost
-
+
{formatTokens(agent.tokenCount)}
tokens
-
+
0 ? "text-red-400" : "text-foreground"}`}>{agent.errorCount}
errors
-
+
{formatRelativeTime(agent.lastHeartbeat)}
heartbeat
-
- {(agent.status === "running" || agent.status === "paused") && ( - - )} -
+ {(agent.status === "running" || agent.status === "paused") && ( +
+ +
+ )}
); })} diff --git a/frontend/src/app/dashboard/page.tsx b/frontend/src/app/dashboard/page.tsx index 8ef7d5d..cf76615 100644 --- a/frontend/src/app/dashboard/page.tsx +++ b/frontend/src/app/dashboard/page.tsx @@ -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 ( -
-
-
- Loading dashboard... +
+ +
+ +
); @@ -618,39 +628,47 @@ function DashboardContent() { {/* Tab Content */} {tab === "agents" && ( - + + + )} {tab === "sessions" && ( - + + + )} {tab === "analytics" && ( - + + + )} {tab === "projects" && ( - + + + )}
diff --git a/frontend/src/components/ui/dashboard-skeletons.tsx b/frontend/src/components/ui/dashboard-skeletons.tsx new file mode 100644 index 0000000..5ba305d --- /dev/null +++ b/frontend/src/components/ui/dashboard-skeletons.tsx @@ -0,0 +1,129 @@ +import { Card, CardContent, CardHeader } from "@/components/ui/card" +import { Skeleton } from "@/components/ui/skeleton" + +export function StatsOverviewSkeleton() { + return ( +
+ {Array.from({ length: 5 }).map((_, i) => ( + + + + + + + + + + ))} +
+ ) +} + +export function AgentsTabSkeleton() { + return ( +
+
+ + +
+
+ {Array.from({ length: 3 }).map((_, i) => ( +
+
+ +
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ ))} +
+
+ ) +} + +export function SessionsTabSkeleton() { + return ( +
+
+ {Array.from({ length: 4 }).map((_, i) => ( + + ))} +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+ + +
+
+ + + +
+
+ ))} +
+ ) +} + +export function AnalyticsTabSkeleton() { + return ( +
+
+ +
+ {Array.from({ length: 5 }).map((_, i) => ( + + ))} +
+
+ + + + + +
+ + + {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+
+ + + {Array.from({ length: 4 }).map((_, i) => ( +
+ + +
+ ))} +
+
+
+
+ ) +} diff --git a/frontend/src/components/ui/error-boundary.tsx b/frontend/src/components/ui/error-boundary.tsx new file mode 100644 index 0000000..defedbe --- /dev/null +++ b/frontend/src/components/ui/error-boundary.tsx @@ -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 { + 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 ( +
+
💥
+
+ Something went wrong +
+
+ {this.state.error?.message || "An unexpected error occurred"} +
+ +
+ ) + } + + return this.props.children + } +} diff --git a/frontend/src/components/ui/skeleton.tsx b/frontend/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..8099a66 --- /dev/null +++ b/frontend/src/components/ui/skeleton.tsx @@ -0,0 +1,12 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton }