feat(frontend): replace dashboard with realtime device map
All checks were successful
Deploy to Production / deploy (push) Successful in 17m7s
All checks were successful
Deploy to Production / deploy (push) Successful in 17m7s
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
import L from "leaflet"
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
Layers,
|
||||
Maximize,
|
||||
@@ -15,13 +17,7 @@ import "leaflet/dist/leaflet.css"
|
||||
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -445,6 +441,7 @@ function Dashboard() {
|
||||
const { snapshot, status, disconnectedAt } = useTelemetryWS()
|
||||
const [baseMap, setBaseMap] = useState<BaseMapType>("amap")
|
||||
const [activeDeviceId, setActiveDeviceId] = useState<string>("")
|
||||
const [isPanelExpanded, setIsPanelExpanded] = useState(true)
|
||||
const activeDeviceIdRef = useRef(activeDeviceId)
|
||||
activeDeviceIdRef.current = activeDeviceId
|
||||
|
||||
@@ -637,89 +634,113 @@ function Dashboard() {
|
||||
})}
|
||||
</MapContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Selected Device Panel — placed at bottom, compact */}
|
||||
<Card className="rounded-none border-x-0 border-b-0 shadow-none bg-background/50 backdrop-blur-sm shrink-0">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
Selected Device
|
||||
{activeDevice?.signal_health &&
|
||||
signalHealthBadge(activeDevice.signal_health)}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Click a marker on the map to view detailed device info
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-4 text-sm sm:grid-cols-2 lg:grid-cols-4 xl:grid-cols-8">
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1">Device ID</p>
|
||||
<p className="font-medium break-all">
|
||||
{activeDevice?.device_id ?? "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1">Latitude</p>
|
||||
<p className="font-medium font-mono">
|
||||
{formatCoord(activeDevice?.lat ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1">Longitude</p>
|
||||
<p className="font-medium font-mono">
|
||||
{formatCoord(activeDevice?.lon ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1 flex items-center gap-1">
|
||||
<Mountain className="size-3" />
|
||||
Altitude
|
||||
</p>
|
||||
<p className="font-medium font-mono">
|
||||
{activeDevice?.alt_m !== null && activeDevice?.alt_m !== undefined
|
||||
? `${activeDevice.alt_m.toFixed(1)} m`
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1">Fix Status</p>
|
||||
<p
|
||||
className={`font-medium ${getFixStatusColor(activeDevice?.fix_status ?? null)}`}
|
||||
{/* Selected Device Panel Overlay */}
|
||||
<div
|
||||
className={`absolute bottom-6 right-6 z-9999 w-80 max-w-[calc(100%-3rem)] transition-all duration-300 ${isPanelExpanded ? "opacity-80" : "opacity-100"}`}
|
||||
>
|
||||
<Card className="shadow-lg border bg-background/50 backdrop-blur-md overflow-hidden">
|
||||
<CardHeader
|
||||
className="p-3 flex flex-row items-center justify-between space-y-0 cursor-pointer select-none"
|
||||
onClick={() => setIsPanelExpanded(!isPanelExpanded)}
|
||||
>
|
||||
{getFixStatusLabel(activeDevice?.fix_status ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1 flex items-center gap-1">
|
||||
<Satellite className="size-3" />
|
||||
Satellites
|
||||
</p>
|
||||
<p className="font-medium font-mono">
|
||||
{activeDevice?.satellites_used ?? "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1 flex items-center gap-1">
|
||||
<Signal className="size-3" />
|
||||
Signal Score
|
||||
</p>
|
||||
<p className="font-medium font-mono">
|
||||
{activeDevice?.signal_health
|
||||
? `${activeDevice.signal_health.score}%`
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-1 flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
Last Seen
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
{timeAgo(activeDevice?.last_event_time ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<CardTitle className="text-sm font-semibold flex items-center gap-2">
|
||||
Selected Device
|
||||
{activeDevice?.signal_health &&
|
||||
!isPanelExpanded &&
|
||||
signalHealthBadge(activeDevice.signal_health)}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1">
|
||||
{activeDevice?.signal_health &&
|
||||
isPanelExpanded &&
|
||||
signalHealthBadge(activeDevice.signal_health)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 hover:bg-transparent"
|
||||
>
|
||||
{isPanelExpanded ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronUp className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
{isPanelExpanded && (
|
||||
<CardContent className="p-3 pt-0 grid grid-cols-2 gap-x-4 gap-y-3 text-[11px]">
|
||||
<div className="col-span-2">
|
||||
<p className="text-muted-foreground mb-0.5">Device ID</p>
|
||||
<p className="font-medium break-all text-xs">
|
||||
{activeDevice?.device_id ?? "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-0.5">Latitude</p>
|
||||
<p className="font-medium font-mono">
|
||||
{formatCoord(activeDevice?.lat ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-0.5">Longitude</p>
|
||||
<p className="font-medium font-mono">
|
||||
{formatCoord(activeDevice?.lon ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-0.5 flex items-center gap-1">
|
||||
<Mountain className="size-3" />
|
||||
Altitude
|
||||
</p>
|
||||
<p className="font-medium font-mono">
|
||||
{activeDevice?.alt_m !== null &&
|
||||
activeDevice?.alt_m !== undefined
|
||||
? `${activeDevice.alt_m.toFixed(1)} m`
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-0.5">Fix Status</p>
|
||||
<p
|
||||
className={`font-medium ${getFixStatusColor(activeDevice?.fix_status ?? null)}`}
|
||||
>
|
||||
{getFixStatusLabel(activeDevice?.fix_status ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-0.5 flex items-center gap-1">
|
||||
<Satellite className="size-3" />
|
||||
Satellites
|
||||
</p>
|
||||
<p className="font-medium font-mono">
|
||||
{activeDevice?.satellites_used ?? "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground mb-0.5 flex items-center gap-1">
|
||||
<Signal className="size-3" />
|
||||
SNR
|
||||
</p>
|
||||
<p className="font-medium font-mono">
|
||||
{activeDevice?.signal_health
|
||||
? `${activeDevice.signal_health.score}%`
|
||||
: "-"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<p className="text-muted-foreground mb-0.5 flex items-center gap-1">
|
||||
<Clock className="size-3" />
|
||||
Last Seen
|
||||
</p>
|
||||
<p className="font-medium">
|
||||
{timeAgo(activeDevice?.last_event_time ?? null)}
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user