feat(ui): replace skeleton loaders with honest loading states

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-21 13:40:58 -05:00
parent 0ee4416077
commit 17037e4936
41 changed files with 101 additions and 275 deletions

View File

@@ -2,7 +2,7 @@ import { RouterProvider, createRouter } from '@tanstack/react-router'
import { useEffect, useState } from 'react'
import { routeTree } from './routeTree.gen'
import { useAuth } from './lib/auth'
import { Skeleton } from './components/ui/skeleton'
import { LoadingText } from './components/ui/skeleton'
const router = createRouter({
routeTree,
@@ -24,17 +24,13 @@ function AppInner() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// Only show skeleton during initial auth check -- NOT on subsequent isLoading changes.
// Only show loading text during initial auth check -- NOT on subsequent isLoading changes.
// Reacting to isLoading here would unmount the entire router tree (including LoginPage)
// every time an auth action sets isLoading, destroying all component local state.
if (!hasChecked) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="space-y-4 w-64">
<Skeleton className="h-8 w-48 mx-auto" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<LoadingText />
</div>
)
}

View File

@@ -74,7 +74,7 @@ export function CertificatesPage() {
Certificate Authority
</h1>
</div>
<TableSkeleton rows={3} />
<TableSkeleton />
</div>
)
}

View File

@@ -189,7 +189,7 @@ export function DeviceCertTable({
}
if (loading) {
return <TableSkeleton rows={4} />
return <TableSkeleton />
}
return (

View File

@@ -64,14 +64,8 @@ export function EntryTable({
if (isLoading) {
return (
<div className="space-y-2">
<div className="flex items-center justify-between mb-3">
<div className="h-5 w-48 bg-elevated/50 rounded animate-pulse" />
<div className="h-8 w-24 bg-elevated/50 rounded animate-pulse" />
</div>
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="h-10 bg-panel rounded animate-pulse" />
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
)
}

View File

@@ -180,7 +180,7 @@ function DeviceSelector({
onSelectionChange(new Set())
}
if (isLoading) return <TableSkeleton rows={5} />
if (isLoading) return <TableSkeleton />
if (devices.length === 0) {
return (

View File

@@ -126,8 +126,8 @@ export function ConfigDiffViewer({
{/* Diff content */}
{isLoading ? (
<div className="p-8 text-center text-sm text-text-muted animate-pulse">
Loading diff...
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
) : !diffFile ? (
<div className="p-8 text-center text-sm text-text-muted">

View File

@@ -83,7 +83,7 @@ export function ConfigHistorySection({ tenantId, deviceId, deviceName }: ConfigH
)}
{isLoading ? (
<TableSkeleton rows={3} />
<TableSkeleton />
) : !changes || changes.length === 0 ? (
<div className="flex items-center justify-center py-6">
<span className="text-xs text-text-muted">No configuration changes recorded yet.</span>

View File

@@ -158,13 +158,8 @@ export function ConfigTab({
{/* Timeline panel */}
<div className="w-72 flex-shrink-0">
{isLoading ? (
<div className="space-y-2">
{[0, 1, 2].map((i) => (
<div
key={i}
className="h-14 rounded-lg border border-border bg-panel animate-pulse"
/>
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
) : !backups || backups.length === 0 ? (
<div className="rounded-lg border border-border bg-panel p-6 text-center text-sm text-text-muted">

View File

@@ -47,10 +47,8 @@ export function DiffViewer({ tenantId, deviceId, snapshotId, onClose }: DiffView
{/* Content */}
{isLoading ? (
<div className="space-y-2">
{[75, 90, 65, 85, 70, 80].map((w, i) => (
<div key={i} className="h-4 bg-elevated rounded animate-pulse" style={{ width: `${w}%` }} />
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
) : isError || !diff ? (
<div className="flex items-center justify-center py-6">

View File

@@ -23,7 +23,7 @@ import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
import {
Dialog,
DialogContent,
@@ -1340,16 +1340,10 @@ function CellEmpty() {
function LoadingRows({ cols }: { cols: number }) {
return (
<>
{Array.from({ length: 5 }).map((_, i) => (
<tr key={i} className="border-b border-border/50">
{Array.from({ length: cols }).map((_, j) => (
<td key={j} className="px-3 py-2">
<Skeleton className="h-4 w-full" />
</td>
))}
</tr>
))}
</>
<tr>
<td colSpan={cols} className="py-8 text-center">
<LoadingText />
</td>
</tr>
)
}

View File

@@ -24,7 +24,7 @@ import { Badge } from '@/components/ui/badge'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Checkbox } from '@/components/ui/checkbox'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
import {
Select,
SelectContent,
@@ -228,23 +228,13 @@ export function InterfacesPanel({ tenantId, deviceId, active }: ConfigPanelProps
}
// ---------------------------------------------------------------------------
// Loading skeleton
// Loading state
// ---------------------------------------------------------------------------
function TableSkeleton({ rows = 5 }: { rows?: number }) {
function TableLoading() {
return (
<div className="rounded-lg border border-border bg-panel">
<div className="p-3 border-b border-border">
<Skeleton className="h-4 w-32" />
</div>
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex items-center gap-4 p-3 border-b border-border last:border-0">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-12" />
</div>
))}
<div className="py-8 text-center">
<LoadingText />
</div>
)
}
@@ -260,7 +250,7 @@ function InterfacesTable({
entries: Record<string, string>[]
isLoading: boolean
}) {
if (isLoading) return <TableSkeleton />
if (isLoading) return <TableLoading />
if (entries.length === 0) {
return (
@@ -362,7 +352,7 @@ function IpAddressesTab({ entries, isLoading, interfaceNames, addChange }: IpAdd
})
}
if (isLoading) return <TableSkeleton />
if (isLoading) return <TableLoading />
return (
<div className="space-y-3">
@@ -621,7 +611,7 @@ function VlansTab({ entries, isLoading, interfaceNames, addChange }: VlansTabPro
})
}
if (isLoading) return <TableSkeleton />
if (isLoading) return <TableLoading />
return (
<div className="space-y-3">
@@ -926,7 +916,7 @@ function BridgesTab({
})
}
if (isLoading) return <TableSkeleton />
if (isLoading) return <TableLoading />
return (
<div className="space-y-6">

View File

@@ -52,10 +52,8 @@ export function RestorePreview({
if (isLoading) {
return (
<div className="space-y-4 p-4">
<div className="h-12 rounded-lg bg-elevated animate-pulse" />
<div className="h-32 rounded-lg bg-elevated animate-pulse" />
<div className="h-16 rounded-lg bg-elevated animate-pulse" />
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
)
}

View File

@@ -10,7 +10,7 @@
import { useMemo } from 'react'
import { Zap, Network } from 'lucide-react'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
import { useConfigBrowse } from '@/hooks/useConfigPanel'
import type { ConfigPanelProps } from '@/lib/configPanelTypes'
@@ -134,12 +134,8 @@ export function SwitchPortManager({ tenantId, deviceId, active }: ConfigPanelPro
if (isLoading) {
return (
<div className="rounded-lg border border-border bg-panel p-4">
<div className="flex flex-wrap gap-2">
{Array.from({ length: 8 }).map((_, i) => (
<Skeleton key={i} className="h-20 w-16 rounded-md" />
))}
</div>
<div className="py-8 text-center">
<LoadingText />
</div>
)
}

View File

@@ -7,7 +7,7 @@ import { alertsApi } from '@/lib/alertsApi'
import { useEventStreamContext } from '@/contexts/EventStreamContext'
import { LayoutDashboard } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
import { EmptyState } from '@/components/ui/empty-state'
// ─── Dashboard Widgets ───────────────────────────────────────────────────────
@@ -30,39 +30,12 @@ const REFRESH_OPTIONS: { label: string; value: RefreshInterval }[] = [
{ label: 'Off', value: false },
]
// ─── Dashboard Skeleton ──────────────────────────────────────────────────────
// ─── Dashboard Loading ───────────────────────────────────────────────────────
function DashboardSkeleton() {
function DashboardLoading() {
return (
<div className="space-y-4">
{/* KPI cards skeleton */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="rounded-lg border border-border p-4">
<Skeleton className="h-3 w-24 mb-2" />
<Skeleton className="h-8 w-16" />
</div>
))}
</div>
{/* Widget grid skeleton */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2 rounded-lg border border-border p-4 space-y-3">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-48 w-full" />
</div>
<div className="rounded-lg border border-border p-4 space-y-3">
<Skeleton className="h-4 w-24" />
<Skeleton className="h-32 w-full" />
</div>
<div className="lg:col-span-2 rounded-lg border border-border p-4 space-y-3">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-48 w-full" />
</div>
<div className="rounded-lg border border-border p-4 space-y-3">
<Skeleton className="h-4 w-28" />
<Skeleton className="h-32 w-full" />
</div>
</div>
<div className="py-8 text-center">
<LoadingText />
</div>
)
}
@@ -236,7 +209,7 @@ export function FleetDashboard() {
{/* ── Dashboard Content ───────────────────────────────────────────── */}
{fleetLoading ? (
<DashboardSkeleton />
<DashboardLoading />
) : totalDevices === 0 ? (
<EmptyState
icon={LayoutDashboard}

View File

@@ -345,7 +345,7 @@ export function FleetTable({
isFetching && !isLoading && 'opacity-70',
)}>
{isLoading ? (
<TableSkeleton rows={3} />
<TableSkeleton />
) : items.length === 0 ? (
<EmptyState
icon={Monitor}

View File

@@ -99,13 +99,8 @@ export function MaintenanceList({ tenantId }: MaintenanceListProps) {
if (isLoading) {
return (
<div className="space-y-4">
{[1, 2, 3].map((i) => (
<div
key={i}
className="h-20 rounded-lg border border-border bg-panel animate-pulse"
/>
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
)
}

View File

@@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'
import { MapPin } from 'lucide-react'
import { metricsApi, tenantsApi } from '@/lib/api'
import { useAuth, isSuperAdmin } from '@/lib/auth'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
import { FleetMap } from './FleetMap'
export function MapPage() {
@@ -55,7 +55,11 @@ export function MapPage() {
}, [superAdmin, selectedTenant, user])
if (devicesLoading) {
return <Skeleton className="h-[calc(100vh-8rem)] w-full rounded-lg" />
return (
<div className="flex items-center justify-center h-[calc(100vh-8rem)]">
<LoadingText />
</div>
)
}
if (devicesError) {

View File

@@ -41,10 +41,8 @@ export function HealthTab({ tenantId, deviceId, active = true }: HealthTabProps)
/>
{isLoading ? (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{[0, 1, 2, 3].map((i) => (
<div key={i} className="rounded-lg border border-border bg-panel p-4 h-44 animate-pulse" />
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
) : !data || data.length === 0 ? (
<div className="rounded-lg border border-border bg-panel p-8 text-center text-sm text-text-muted">

View File

@@ -87,10 +87,8 @@ export function InterfacesTab({ tenantId, deviceId, active = true }: InterfacesT
{/* Charts */}
{isLoading ? (
<div className="space-y-4">
{[0, 1, 2].map((i) => (
<div key={i} className="rounded-lg border border-border bg-panel p-4 h-56 animate-pulse" />
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
) : !trafficData || trafficData.length === 0 ? (
<div className="rounded-lg border border-border bg-panel p-8 text-center text-sm text-text-muted">

View File

@@ -159,10 +159,8 @@ export function WirelessTab({ tenantId, deviceId, active = true }: WirelessTabPr
/>
{isLoading ? (
<div className="space-y-4">
{[0, 1].map((i) => (
<div key={i} className="rounded-lg border border-border bg-panel p-4 h-48 animate-pulse" />
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
) : hasNoWireless ? (
<div className="rounded-lg border border-border bg-panel p-8 text-center text-sm text-text-muted">

View File

@@ -171,7 +171,7 @@ export function ClientsTab({ tenantId, deviceId, active }: ClientsTabProps) {
// Loading state
if (isLoading) {
return <TableSkeleton rows={8} />
return <TableSkeleton />
}
// Error state

View File

@@ -1,6 +1,6 @@
import { useQuery } from '@tanstack/react-query'
import { metricsApi, type InterfaceMetricPoint } from '@/lib/api'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
interface InterfaceGaugesProps {
tenantId: string
@@ -101,14 +101,8 @@ export function InterfaceGauges({ tenantId, deviceId, active }: InterfaceGaugesP
if (isLoading) {
return (
<div className="space-y-3">
{[0, 1, 2].map((i) => (
<div key={i} className="rounded-lg border border-border bg-panel p-3">
<Skeleton className="h-4 w-24 mb-2" />
<Skeleton className="h-3 w-full mb-1" />
<Skeleton className="h-3 w-full" />
</div>
))}
<div className="py-8 text-center">
<LoadingText />
</div>
)
}

View File

@@ -2,7 +2,7 @@ import { useState, useMemo, useCallback, useRef, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Search, RefreshCw } from 'lucide-react'
import { networkApi, type LogEntry } from '@/lib/networkApi'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
interface LogsTabProps {
tenantId: string
@@ -62,16 +62,10 @@ function TopicBadge({ topics }: { topics: string }) {
)
}
function TableSkeleton() {
function TableLoading() {
return (
<div className="space-y-1">
{Array.from({ length: 8 }).map((_, i) => (
<div key={i} className="flex gap-3 py-2 px-3">
<Skeleton className="h-4 w-32 shrink-0" />
<Skeleton className="h-4 w-20 shrink-0" />
<Skeleton className="h-4 flex-1" />
</div>
))}
<div className="py-8 text-center">
<LoadingText />
</div>
)
}
@@ -189,7 +183,7 @@ export function LogsTab({ tenantId, deviceId, active }: LogsTabProps) {
{/* Log table */}
<div className="rounded-lg border border-border bg-panel overflow-hidden">
{isLoading ? (
<TableSkeleton />
<TableLoading />
) : error ? (
<div className="p-6 text-center text-sm text-error">
Failed to fetch device logs. The device may be offline or unreachable.

View File

@@ -1,7 +1,7 @@
import { useQuery } from '@tanstack/react-query'
import { Shield, Lock, Globe } from 'lucide-react'
import { networkApi, type VpnTunnel } from '@/lib/networkApi'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
import { Badge } from '@/components/ui/badge'
interface VpnTabProps {
@@ -102,10 +102,8 @@ export function VpnTab({ tenantId, deviceId, active }: VpnTabProps) {
if (isLoading) {
return (
<div className="mt-4 space-y-2">
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<Skeleton className="h-10 w-full" />
<div className="mt-4 py-8 text-center">
<LoadingText />
</div>
)
}

View File

@@ -611,7 +611,7 @@ function DeviceSelectionStep({
}
if (devicesLoading) {
return <TableSkeleton rows={5} />
return <TableSkeleton />
}
return (

View File

@@ -16,7 +16,7 @@ export function SimpleStatusBanner({ items, isLoading }: SimpleStatusBannerProps
<div key={i} className="flex flex-col">
<span className="text-xs text-text-muted">{item.label}</span>
{isLoading ? (
<div className="h-5 w-24 mt-0.5 rounded bg-elevated animate-pulse" />
<span className="text-[9px] text-text-muted mt-0.5">Loading&hellip;</span>
) : (
<span className="text-sm font-medium text-text-primary">
{item.value || '\u2014'}

View File

@@ -57,15 +57,8 @@ export function SiteHealthGrid({ tenantId, siteId }: SiteHealthGridProps) {
if (devicesLoading) {
return (
<div className="grid grid-cols-[repeat(auto-fill,minmax(200px,1fr))] gap-3">
{Array.from({ length: 6 }).map((_, i) => (
<div key={i} className="rounded-lg border border-border bg-panel p-4 space-y-3 animate-pulse">
<div className="h-4 w-24 bg-elevated rounded" />
<div className="h-1.5 w-full bg-elevated rounded-full" />
<div className="h-1.5 w-full bg-elevated rounded-full" />
<div className="h-3 w-16 bg-elevated rounded" />
</div>
))}
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
)
}

View File

@@ -145,7 +145,7 @@ export function SiteSectorView({ tenantId, siteId }: SiteSectorViewProps) {
}, [linksData])
if (sectorsLoading || devicesLoading) {
return <TableSkeleton rows={6} />
return <TableSkeleton />
}
const sectors = sectorData?.items ?? []

View File

@@ -99,7 +99,7 @@ export function SiteTable({ tenantId, search, onCreateClick, onEditClick }: Site
}
if (isLoading) {
return <TableSkeleton rows={4} />
return <TableSkeleton />
}
if (!data || data.sites.length === 0) {

View File

@@ -74,7 +74,7 @@ export function TenantList() {
{isLoading ? (
<tr>
<td colSpan={5} className="px-3 py-4">
<TableSkeleton rows={5} />
<TableSkeleton />
</td>
</tr>
) : tenants?.length === 0 ? (

View File

@@ -1,84 +1,13 @@
import { Skeleton } from './skeleton'
import { LoadingText } from './skeleton'
/** Table-style skeleton with header + rows */
export function TableSkeleton({ rows = 8 }: { rows?: number }) {
return (
<div className="space-y-3">
{/* Header */}
<div className="flex items-center gap-4">
<Skeleton className="h-8 w-48" />
<Skeleton className="h-8 w-32 ml-auto" />
</div>
{/* Filter bar */}
<div className="flex gap-2">
<Skeleton className="h-9 w-64" />
<Skeleton className="h-9 w-32" />
</div>
{/* Table header */}
<div className="flex gap-4 py-2">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-20" />
<Skeleton className="h-4 w-28" />
<Skeleton className="h-4 w-16 ml-auto" />
</div>
{/* Rows */}
{Array.from({ length: rows }).map((_, i) => (
<div key={i} className="flex items-center gap-4 py-3">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-28" />
<Skeleton className="h-4 w-16" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-8 w-20 ml-auto" />
</div>
))}
</div>
)
export function TableSkeleton() {
return <div className="py-8 text-center"><LoadingText /></div>
}
/** Card grid skeleton */
export function CardGridSkeleton({ cards = 6 }: { cards?: number }) {
return (
<div className="space-y-4">
<Skeleton className="h-8 w-48" />
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{Array.from({ length: cards }).map((_, i) => (
<div key={i} className="rounded-lg border border-border p-4 space-y-3">
<Skeleton className="h-5 w-3/4" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-16 w-full" />
</div>
))}
</div>
</div>
)
export function CardGridSkeleton() {
return <div className="py-8 text-center"><LoadingText /></div>
}
/** Detail page skeleton (header + tabs + content) */
export function DetailPageSkeleton() {
return (
<div className="space-y-4">
<div className="flex items-center gap-3">
<Skeleton className="h-8 w-8 rounded-full" />
<div className="space-y-1">
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-32" />
</div>
<Skeleton className="h-9 w-24 ml-auto" />
</div>
{/* Tabs */}
<div className="flex gap-4 border-b border-border pb-2">
<Skeleton className="h-5 w-20" />
<Skeleton className="h-5 w-20" />
<Skeleton className="h-5 w-20" />
<Skeleton className="h-5 w-20" />
</div>
{/* Content area */}
<div className="grid grid-cols-2 gap-4">
<Skeleton className="h-48 w-full" />
<Skeleton className="h-48 w-full" />
</div>
<Skeleton className="h-64 w-full" />
</div>
)
return <div className="py-8 text-center"><LoadingText /></div>
}

View File

@@ -1,15 +1,11 @@
import { cn } from '@/lib/utils'
type SkeletonProps = React.HTMLAttributes<HTMLDivElement>
export function Skeleton({ className, ...props }: SkeletonProps) {
return (
<div
className={cn(
'animate-pulse rounded-md bg-elevated relative overflow-hidden',
className,
)}
{...props}
/>
)
// Compatibility shim — returns null. Remove once all imports are cleaned up.
export function Skeleton({ className: _className }: { className?: string }) {
return null
}
// Use this for panels where loading delay is noticeable
export function LoadingText({ text = 'Loading\u2026' }: { text?: string }) {
return <span className="text-[9px] text-text-muted">{text}</span>
}

View File

@@ -91,7 +91,7 @@ export function UserList({ tenantId }: Props) {
{isLoading ? (
<tr>
<td colSpan={6} className="px-3 py-4">
<TableSkeleton rows={5} />
<TableSkeleton />
</td>
</tr>
) : users?.length === 0 ? (

View File

@@ -190,7 +190,7 @@ export function VpnPage() {
return (
<div className="p-6 space-y-6">
<h1 className="text-2xl font-bold text-text-primary">VPN</h1>
<TableSkeleton rows={3} />
<TableSkeleton />
</div>
)
}
@@ -309,7 +309,7 @@ export function VpnPage() {
{/* Peer list */}
{peersLoading ? (
<TableSkeleton rows={3} />
<TableSkeleton />
) : peers.length === 0 ? (
<div className="rounded-lg border border-dashed border-accent/30 bg-accent/5 p-8 text-center space-y-3">
<ShieldCheck className="h-10 w-10 text-accent mx-auto" />

View File

@@ -29,7 +29,7 @@ export function RFStatsCard({ tenantId, deviceId, active }: RFStatsCardProps) {
})
if (isLoading) {
return <TableSkeleton rows={2} />
return <TableSkeleton />
}
if (!data || data.items.length === 0) {

View File

@@ -88,7 +88,7 @@ export function WirelessLinksTable({ tenantId, siteId }: WirelessLinksTableProps
}, [data])
if (isLoading) {
return <TableSkeleton rows={8} />
return <TableSkeleton />
}
if (!data || data.items.length === 0) {

View File

@@ -44,7 +44,7 @@ export function WirelessStationTable({ tenantId, deviceId, active }: WirelessSta
})
if (isLoading) {
return <TableSkeleton rows={5} />
return <TableSkeleton />
}
if (!data || data.items.length === 0) {

View File

@@ -12,7 +12,7 @@ import { EventStreamProvider } from '@/contexts/EventStreamContext'
import type { FleetDevice } from '@/lib/api'
import { AppLayout } from '@/components/layout/AppLayout'
import { PageTransition } from '@/components/layout/PageTransition'
import { Skeleton } from '@/components/ui/skeleton'
import { LoadingText } from '@/components/ui/skeleton'
import { ErrorBoundary } from '@/components/ui/error-boundary'
export const Route = createFileRoute('/_authenticated')({
@@ -164,11 +164,7 @@ function AuthenticatedLayout() {
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-screen bg-background">
<div className="space-y-4 w-64">
<Skeleton className="h-8 w-48 mx-auto" />
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
<LoadingText />
</div>
)
}

View File

@@ -800,7 +800,7 @@ function DeviceAlertsSection({
const resolvedAlerts = alerts.filter((a) => a.status === 'resolved').slice(0, 5)
if (isLoading) {
return <TableSkeleton rows={3} />
return <TableSkeleton />
}
return (

View File

@@ -23,7 +23,7 @@ function TenantDetailPage() {
})
if (isLoading) {
return <CardGridSkeleton cards={3} />
return <CardGridSkeleton />
}
if (!tenant) {

View File

@@ -26,9 +26,8 @@ function SiteDetailPage() {
if (isLoading) {
return (
<div className="animate-pulse space-y-4">
<div className="h-6 w-48 bg-elevated rounded" />
<div className="h-4 w-64 bg-elevated rounded" />
<div className="py-8 text-center">
<span className="text-[9px] text-text-muted">Loading&hellip;</span>
</div>
)
}