Skip to content

Commit c8cce2b

Browse files
committed
feat: build routes explorer UI and route inspection workspace
1 parent 55af980 commit c8cce2b

9 files changed

Lines changed: 503 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { RoutesWorkspace } from "@/components/routes/routes-workspace"
2+
3+
export default function RoutesPage() {
4+
return <RoutesWorkspace />
5+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"use client"
2+
3+
import { Route } from "@/types/route"
4+
import { Route as RouteIcon } from "lucide-react"
5+
type Props = {
6+
route: Route | null
7+
}
8+
9+
export function RouteInspector({
10+
route,
11+
}: Props) {
12+
if (!route) {
13+
return (
14+
<div className="flex h-full items-center justify-center text-muted-foreground">
15+
Select a route
16+
</div>
17+
)
18+
}
19+
20+
return (
21+
<div className="flex h-full flex-col">
22+
23+
<div className="border-b border-border p-5">
24+
25+
<div className="flex items-center gap-3">
26+
27+
<RouteIcon className="h-5 w-5 text-orange-400" />
28+
29+
<div>
30+
<h2 className="font-semibold">
31+
Route Inspector
32+
</h2>
33+
34+
<p className="text-sm text-muted-foreground">
35+
route details
36+
</p>
37+
</div>
38+
39+
</div>
40+
41+
</div>
42+
43+
<div className="space-y-4 p-5 text-sm">
44+
45+
<Info
46+
label="Provider"
47+
value={route.provider}
48+
/>
49+
50+
<Info
51+
label="Status"
52+
value={route.status}
53+
/>
54+
55+
<Info
56+
label="Throughput"
57+
value={`${route.throughput}/m`}
58+
/>
59+
60+
<Info
61+
label="Failures"
62+
value={route.failures}
63+
/>
64+
65+
<Info
66+
label="Destinations"
67+
value={route.destinations}
68+
/>
69+
70+
</div>
71+
72+
<div className="border-t border-border p-5">
73+
74+
<h3 className="mb-3 font-medium">
75+
Destinations
76+
</h3>
77+
78+
<div className="space-y-2 text-sm">
79+
<div>production-api</div>
80+
<div>analytics-worker</div>
81+
<div>audit-logger</div>
82+
</div>
83+
84+
</div>
85+
86+
</div>
87+
)
88+
}
89+
90+
function Info({
91+
label,
92+
value,
93+
}: {
94+
label: string
95+
value: React.ReactNode
96+
}) {
97+
return (
98+
<div className="flex justify-between">
99+
<span>{label}</span>
100+
<span>{value}</span>
101+
</div>
102+
)
103+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"use client"
2+
3+
import { cn } from "@/lib/utils"
4+
import { Route } from "@/types/route"
5+
6+
type Props = {
7+
route: Route
8+
selected?: boolean
9+
onClick?: () => void
10+
}
11+
12+
export function RouteRow({
13+
route,
14+
selected,
15+
onClick,
16+
}: Props) {
17+
return (
18+
<button
19+
onClick={onClick}
20+
className={cn(
21+
`
22+
grid
23+
grid-cols-[120px_1fr_120px_120px_120px_120px_140px]
24+
items-center
25+
border-b border-border
26+
px-5 py-4
27+
text-left
28+
hover:bg-white/[0.03]
29+
`,
30+
selected &&
31+
"bg-white/[0.04]"
32+
)}
33+
>
34+
<div>{route.provider}</div>
35+
36+
<div>{route.path}</div>
37+
38+
<div>
39+
<span
40+
className={cn(
41+
"rounded-full px-2 py-1 text-xs",
42+
43+
route.status ===
44+
"active" &&
45+
"bg-emerald-500/10 text-emerald-400",
46+
47+
route.status ===
48+
"paused" &&
49+
"bg-amber-500/10 text-amber-400",
50+
51+
route.status ===
52+
"error" &&
53+
"bg-rose-500/10 text-rose-400"
54+
)}
55+
>
56+
{route.status}
57+
</span>
58+
</div>
59+
60+
<div>{route.throughput}/m</div>
61+
62+
<div>{route.failures}</div>
63+
64+
<div>{route.destinations}</div>
65+
66+
<div>{route.lastSeen}</div>
67+
</button>
68+
)
69+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client"
2+
3+
import { Route } from "@/types/route"
4+
5+
type Props = {
6+
routes: Route[]
7+
}
8+
9+
export function RoutesStats({
10+
routes,
11+
}: Props) {
12+
const active =
13+
routes.filter(
14+
(r) => r.status === "active"
15+
).length
16+
17+
const paused =
18+
routes.filter(
19+
(r) => r.status === "paused"
20+
).length
21+
22+
const errors =
23+
routes.filter(
24+
(r) => r.status === "error"
25+
).length
26+
27+
return (
28+
<div className="grid grid-cols-4 border-b border-border">
29+
30+
<Stat
31+
label="Total Routes"
32+
value={routes.length}
33+
/>
34+
35+
<Stat
36+
label="Active"
37+
value={active}
38+
/>
39+
40+
<Stat
41+
label="Paused"
42+
value={paused}
43+
/>
44+
45+
<Stat
46+
label="Errors"
47+
value={errors}
48+
/>
49+
50+
</div>
51+
)
52+
}
53+
54+
function Stat({
55+
label,
56+
value,
57+
}: {
58+
label: string
59+
value: number
60+
}) {
61+
return (
62+
<div className="border-r border-border p-5 last:border-r-0">
63+
<p className="text-sm text-muted-foreground">
64+
{label}
65+
</p>
66+
67+
<h3 className="mt-2 text-4xl font-bold">
68+
{value}
69+
</h3>
70+
</div>
71+
)
72+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"use client"
2+
3+
import { Route } from "@/types/route"
4+
import { RouteRow } from "./route-row"
5+
6+
type Props = {
7+
routes: Route[]
8+
selected: Route | null
9+
onSelect: (route: Route) => void
10+
}
11+
12+
export function RoutesStream({
13+
routes,
14+
selected,
15+
onSelect,
16+
}: Props) {
17+
return (
18+
<div className="h-full overflow-auto">
19+
20+
<div
21+
className="
22+
grid
23+
grid-cols-[120px_1fr_120px_120px_120px_120px_140px]
24+
border-b border-border
25+
px-5 py-4
26+
text-xs uppercase
27+
text-muted-foreground
28+
"
29+
>
30+
<div>Provider</div>
31+
<div>Route</div>
32+
<div>Status</div>
33+
<div>Throughput</div>
34+
<div>Failures</div>
35+
<div>Destinations</div>
36+
<div>Last Seen</div>
37+
</div>
38+
39+
{routes.map((route) => (
40+
<RouteRow
41+
key={route.id}
42+
route={route}
43+
selected={
44+
selected?.id ===
45+
route.id
46+
}
47+
onClick={() =>
48+
onSelect(route)
49+
}
50+
/>
51+
))}
52+
</div>
53+
)
54+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use client"
2+
3+
import { Search, Route } from "lucide-react"
4+
5+
type Props = {
6+
query: string
7+
setQuery: React.Dispatch<React.SetStateAction<string>>
8+
}
9+
10+
export function RoutesToolbar({
11+
query,
12+
setQuery,
13+
}: Props) {
14+
return (
15+
<div className="flex items-center justify-between border-b border-border px-5 py-4">
16+
17+
<div className="flex items-center gap-3">
18+
<Route className="h-5 w-5 text-orange-400" />
19+
20+
<div>
21+
<h2 className="text-xl font-semibold">
22+
Routes Explorer
23+
</h2>
24+
25+
<p className="text-sm text-muted-foreground">
26+
monitor ingress routes
27+
</p>
28+
</div>
29+
</div>
30+
31+
<div className="flex items-center gap-2 rounded-xl border border-border px-3 py-2">
32+
<Search className="h-4 w-4 text-muted-foreground" />
33+
34+
<input
35+
value={query}
36+
onChange={(e) =>
37+
setQuery(e.target.value)
38+
}
39+
placeholder="Search routes..."
40+
className="bg-transparent outline-none"
41+
/>
42+
</div>
43+
44+
</div>
45+
)
46+
}

0 commit comments

Comments
 (0)