Openstatus
www.openstatus.dev
1"use client";
2
3import { IconCloudProvider } from "@/components/common/icon-cloud-provider";
4import { BlockWrapper } from "@/components/content/block-wrapper";
5import { TableCellDate } from "@/components/data-table/table-cell-date";
6import { TableCellNumber } from "@/components/data-table/table-cell-number";
7import {
8 Table,
9 TableBody,
10 TableCell,
11 TableHead,
12 TableRow,
13} from "@/components/ui/table";
14import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
15import { getStatusCodeVariant, textColors } from "@/data/status-codes";
16import { formatMilliseconds, formatPercentage } from "@/lib/formatter";
17import { cn } from "@/lib/utils";
18import type { RouterOutputs } from "@openstatus/api";
19import type { PrivateLocation } from "@openstatus/db/src/schema";
20import { getRegionInfo } from "@openstatus/regions";
21import { Braces, TableProperties } from "lucide-react";
22
23type ResponseLog = RouterOutputs["tinybird"]["get"]["data"][number];
24
25export function DataTableBasics({
26 data,
27 privateLocations,
28}: {
29 data: ResponseLog;
30 privateLocations?: PrivateLocation[];
31}) {
32 if (data.type === "http") {
33 return (
34 <DataTableBasicsHTTP data={data} privateLocations={privateLocations} />
35 );
36 }
37 if (data.type === "tcp") {
38 return (
39 <DataTableBasicsTCP data={data} privateLocations={privateLocations} />
40 );
41 }
42 if (data.type === "dns") {
43 return (
44 <DataTableBasicsDNS data={data} privateLocations={privateLocations} />
45 );
46 }
47 return null;
48}
49
50export function DataTableBasicsHTTP({
51 data,
52 privateLocations,
53}: {
54 data: Extract<ResponseLog, { type: "http" }> & {
55 trigger?: "cron" | "api" | "test" | null;
56 };
57 privateLocations?: PrivateLocation[];
58}) {
59 const privateLocataion = privateLocations?.find(
60 (location) => String(location.id) === String(data.region),
61 );
62 const regionConfig = getRegionInfo(data.region, {
63 location: privateLocataion?.name,
64 });
65 return (
66 <Table className="table-fixed">
67 <colgroup>
68 <col className="w-1/3" />
69 <col className="w-2/3" />
70 </colgroup>
71 <TableBody>
72 <TableRow>
73 <TableHead colSpan={2}>Request</TableHead>
74 </TableRow>
75 <TableRow className="[&>:not(:last-child)]:border-r">
76 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
77 Result
78 </TableHead>
79 {/* TODO: add colored square like list (see columns) */}
80 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
81 <div className="flex items-center gap-2">
82 <div
83 className={cn("h-2.5 w-2.5 rounded-[2px] bg-muted", {
84 "bg-destructive": data?.requestStatus === "error",
85 "bg-warning": data?.requestStatus === "degraded",
86 "bg-success": data?.requestStatus === "success",
87 })}
88 />
89 <div className="capitalize">
90 {data?.requestStatus ?? "unknown"}
91 </div>
92 </div>
93 </TableCell>
94 </TableRow>
95 {data.id ? (
96 <TableRow className="[&>:not(:last-child)]:border-r">
97 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
98 ID
99 </TableHead>
100 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
101 {data.id}
102 </TableCell>
103 </TableRow>
104 ) : null}
105 <TableRow className="[&>:not(:last-child)]:border-r">
106 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
107 Timestamp
108 </TableHead>
109 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
110 <TableCellDate
111 value={new Date(data.cronTimestamp)}
112 className="text-foreground"
113 />
114 </TableCell>
115 </TableRow>
116 <TableRow className="[&>:not(:last-child)]:border-r">
117 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
118 URL
119 </TableHead>
120 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
121 {data.url}
122 </TableCell>
123 </TableRow>
124 {/* TODO: store method in TB 🤦 */}
125 {/* <TableRow className="[&>:not(:last-child)]:border-r">
126 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
127 Method
128 </TableHead>
129 <TableCell className="whitespace-normal font-mono">
130 {data?.method}
131 </TableCell>
132 </TableRow> */}
133 <TableRow className="[&>:not(:last-child)]:border-r">
134 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
135 Status
136 </TableHead>
137 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
138 <TableCellNumber
139 value={data.statusCode}
140 className={textColors[getStatusCodeVariant(data.statusCode)]}
141 />
142 </TableCell>
143 </TableRow>
144 <TableRow className="[&>:not(:last-child)]:border-r">
145 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
146 Latency
147 </TableHead>
148 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
149 <TableCellNumber value={data?.latency} unit="ms" />
150 </TableCell>
151 </TableRow>
152 <TableRow className="[&>:not(:last-child)]:border-r">
153 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
154 Region
155 </TableHead>
156 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
157 {regionConfig?.code}{" "}
158 <span className="text-muted-foreground text-xs">
159 {regionConfig?.location} {regionConfig?.flag}
160 </span>
161 </TableCell>
162 </TableRow>
163 <TableRow className="[&>:not(:last-child)]:border-r">
164 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
165 Cloud Provider
166 </TableHead>
167 <TableCell className="inline-flex max-w-full overflow-x-auto whitespace-normal font-mono">
168 <IconCloudProvider
169 provider={regionConfig?.provider}
170 className="mt-0.5"
171 />
172 <span className="ml-1 text-muted-foreground">
173 {regionConfig?.provider}
174 </span>
175 </TableCell>
176 </TableRow>
177 {data.trigger ? (
178 <TableRow className="[&>:not(:last-child)]:border-r">
179 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
180 Trigger
181 </TableHead>
182 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
183 {data?.trigger}
184 </TableCell>
185 </TableRow>
186 ) : null}
187 {data.headers ? (
188 <>
189 <TableRow>
190 <TableHead colSpan={2}>Headers</TableHead>
191 </TableRow>
192 <TableRow className="hover:bg-transparent">
193 <TableCell colSpan={2} className="p-0">
194 <Tabs defaultValue="table" className="w-full gap-0">
195 <TabsList className="w-full justify-start rounded-none border-b px-2">
196 <TabsTrigger value="table">
197 <TableProperties className="size-3 rotate-180" />
198 </TabsTrigger>
199 <TabsTrigger value="raw">
200 <Braces className="size-3" />
201 </TabsTrigger>
202 </TabsList>
203 <TabsContent value="table">
204 <Table className="table-fixed">
205 <colgroup>
206 <col className="w-1/3" />
207 <col className="w-2/3" />
208 </colgroup>
209 <TableBody>
210 {Object.entries(data?.headers ?? {}).map(
211 ([key, value]) => (
212 <TableRow
213 key={key}
214 className="[&>:not(:last-child)]:border-r"
215 >
216 <TableHead className="overflow-x-auto bg-muted/50 font-normal text-muted-foreground">
217 {key}
218 </TableHead>
219 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
220 {value}
221 </TableCell>
222 </TableRow>
223 ),
224 )}
225 </TableBody>
226 </Table>
227 </TabsContent>
228 <TabsContent value="raw">
229 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-4 font-mono text-sm">
230 {JSON.stringify(data?.headers, null, 2)}
231 </pre>
232 </TabsContent>
233 </Tabs>
234 </TableCell>
235 </TableRow>
236 </>
237 ) : null}
238 {data.timing ? (
239 <>
240 <TableRow>
241 <TableHead colSpan={2}>Timing</TableHead>
242 </TableRow>
243 {Object.entries(data?.timing ?? {}).map(([key, value], index) => (
244 <TableRow key={key} className="[&>:not(:last-child)]:border-r">
245 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
246 <span className="uppercase">{key}</span>
247 </TableHead>
248 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
249 <div className="flex items-center justify-between gap-2">
250 <div className="flex-1">
251 <span className="text-muted-foreground">
252 {formatPercentage(value / (data?.latency || 100))}
253 </span>
254 </div>
255 <div className="flex w-full flex-1 items-center justify-end gap-2">
256 <span className="text-nowrap text-muted-foreground">
257 {formatMilliseconds(value)}
258 </span>
259 <div
260 className="h-4"
261 style={{
262 width: `${(value / (data?.latency || 100)) * 100}%`,
263 backgroundColor: `var(--chart-${index + 1})`,
264 }}
265 />
266 </div>
267 </div>
268 </TableCell>
269 </TableRow>
270 ))}
271 </>
272 ) : null}
273 {data?.message ? (
274 <>
275 <TableRow>
276 <TableHead colSpan={2}>Message</TableHead>
277 </TableRow>
278 <TableRow>
279 <TableCell colSpan={2} className="p-0">
280 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm">
281 {data.message}
282 </pre>
283 </TableCell>
284 </TableRow>
285 </>
286 ) : null}
287 {data.body ? (
288 <>
289 <TableRow>
290 <TableHead colSpan={2}>Body</TableHead>
291 </TableRow>
292 <TableRow>
293 <TableCell colSpan={2} className="p-0">
294 <BlockWrapper autoOpen>
295 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm">
296 {data.body}
297 </pre>
298 </BlockWrapper>
299 </TableCell>
300 </TableRow>
301 </>
302 ) : null}
303 {data.assertions ? (
304 <>
305 <TableRow>
306 <TableHead colSpan={2}>Assertions</TableHead>
307 </TableRow>
308 <TableRow>
309 <TableCell colSpan={2} className="p-0">
310 {!data.assertions || data.assertions === "[]" ? (
311 <div className="p-2 font-mono text-muted-foreground text-sm">
312 Default status code 2xx assertion
313 </div>
314 ) : (
315 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm">
316 {JSON.stringify(data.assertions, null, 2)}
317 </pre>
318 )}
319 </TableCell>
320 </TableRow>
321 </>
322 ) : null}
323 </TableBody>
324 </Table>
325 );
326}
327
328export function DataTableBasicsTCP({
329 data,
330 privateLocations,
331}: {
332 data: Extract<ResponseLog, { type: "tcp" }> & {
333 trigger?: "cron" | "api" | "test" | null;
334 };
335 privateLocations?: PrivateLocation[];
336}) {
337 const privateLocataion = privateLocations?.find(
338 (location) => String(location.id) === String(data.region),
339 );
340 const regionConfig = getRegionInfo(data.region, {
341 location: privateLocataion?.name,
342 });
343 return (
344 <Table className="table-fixed">
345 <colgroup>
346 <col className="w-1/3" />
347 <col className="w-2/3" />
348 </colgroup>
349 <TableBody>
350 <TableRow>
351 <TableHead colSpan={2}>Request</TableHead>
352 </TableRow>
353 <TableRow className="[&>:not(:last-child)]:border-r">
354 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
355 Result
356 </TableHead>
357 {/* TODO: add colored square like list (see columns) */}
358 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
359 <div className="flex items-center gap-2">
360 <div
361 className={cn("h-2.5 w-2.5 rounded-[2px] bg-muted", {
362 "bg-destructive": data?.requestStatus === "error",
363 "bg-warning": data?.requestStatus === "degraded",
364 "bg-success": data?.requestStatus === "success",
365 })}
366 />
367 <div className="capitalize">
368 {data?.requestStatus ?? "unknown"}
369 </div>
370 </div>
371 </TableCell>
372 </TableRow>
373 {data.id ? (
374 <TableRow className="[&>:not(:last-child)]:border-r">
375 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
376 ID
377 </TableHead>
378 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
379 {data.id}
380 </TableCell>
381 </TableRow>
382 ) : null}
383 <TableRow className="[&>:not(:last-child)]:border-r">
384 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
385 Timestamp
386 </TableHead>
387 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
388 <TableCellDate
389 value={new Date(data.cronTimestamp)}
390 className="text-foreground"
391 />
392 </TableCell>
393 </TableRow>
394 <TableRow className="[&>:not(:last-child)]:border-r">
395 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
396 URI
397 </TableHead>
398 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
399 {data.uri}
400 </TableCell>
401 </TableRow>
402 <TableRow className="[&>:not(:last-child)]:border-r">
403 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
404 Latency
405 </TableHead>
406 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
407 <TableCellNumber value={data?.latency} unit="ms" />
408 </TableCell>
409 </TableRow>
410 <TableRow className="[&>:not(:last-child)]:border-r">
411 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
412 Region
413 </TableHead>
414 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
415 {regionConfig?.flag} {regionConfig?.code}{" "}
416 <span className="text-muted-foreground">
417 {regionConfig?.location}
418 </span>
419 </TableCell>
420 </TableRow>
421 <TableRow className="[&>:not(:last-child)]:border-r">
422 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
423 Cloud Provider
424 </TableHead>
425 <TableCell className="inline-flex max-w-full overflow-x-auto whitespace-normal font-mono">
426 <IconCloudProvider
427 provider={regionConfig?.provider}
428 className="mt-0.5"
429 />
430 <span className="ml-1 text-muted-foreground">
431 {regionConfig?.provider}
432 </span>
433 </TableCell>
434 </TableRow>
435 {data.trigger ? (
436 <TableRow className="[&>:not(:last-child)]:border-r">
437 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
438 Trigger
439 </TableHead>
440 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
441 {data?.trigger}
442 </TableCell>
443 </TableRow>
444 ) : null}
445 {data?.errorMessage ? (
446 <>
447 <TableRow>
448 <TableHead colSpan={2}>Error Message</TableHead>
449 </TableRow>
450 <TableRow>
451 <TableCell colSpan={2} className="p-0">
452 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm">
453 {data.errorMessage}
454 </pre>
455 </TableCell>
456 </TableRow>
457 </>
458 ) : null}
459 </TableBody>
460 </Table>
461 );
462}
463
464export function DataTableBasicsDNS({
465 data,
466 privateLocations,
467}: {
468 data: Extract<ResponseLog, { type: "dns" }> & {
469 trigger?: "cron" | "api" | "test" | null;
470 };
471 privateLocations?: PrivateLocation[];
472}) {
473 const privateLocataion = privateLocations?.find(
474 (location) => String(location.id) === String(data.region),
475 );
476 const regionConfig = getRegionInfo(data.region, {
477 location: privateLocataion?.name,
478 });
479 return (
480 <Table className="table-fixed">
481 <colgroup>
482 <col className="w-1/3" />
483 <col className="w-2/3" />
484 </colgroup>
485 <TableBody>
486 <TableRow>
487 <TableHead colSpan={2}>Request</TableHead>
488 </TableRow>
489 <TableRow className="[&>:not(:last-child)]:border-r">
490 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
491 Result
492 </TableHead>
493 {/* TODO: add colored square like list (see columns) */}
494 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
495 <div className="flex items-center gap-2">
496 <div
497 className={cn("h-2.5 w-2.5 rounded-[2px] bg-muted", {
498 "bg-destructive": data?.requestStatus === "error",
499 "bg-warning": data?.requestStatus === "degraded",
500 "bg-success": data?.requestStatus === "success",
501 })}
502 />
503 <div className="capitalize">
504 {data?.requestStatus ?? "unknown"}
505 </div>
506 </div>
507 </TableCell>
508 </TableRow>
509 {data.id ? (
510 <TableRow className="[&>:not(:last-child)]:border-r">
511 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
512 ID
513 </TableHead>
514 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
515 {data.id}
516 </TableCell>
517 </TableRow>
518 ) : null}
519 <TableRow className="[&>:not(:last-child)]:border-r">
520 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
521 Timestamp
522 </TableHead>
523 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
524 <TableCellDate
525 value={new Date(data.cronTimestamp)}
526 className="text-foreground"
527 />
528 </TableCell>
529 </TableRow>
530 <TableRow className="[&>:not(:last-child)]:border-r">
531 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
532 URI
533 </TableHead>
534 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
535 {data.uri}
536 </TableCell>
537 </TableRow>
538 <TableRow className="[&>:not(:last-child)]:border-r">
539 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
540 Latency
541 </TableHead>
542 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
543 <TableCellNumber value={data?.latency} unit="ms" />
544 </TableCell>
545 </TableRow>
546 <TableRow className="[&>:not(:last-child)]:border-r">
547 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
548 Region
549 </TableHead>
550 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
551 {regionConfig?.flag} {regionConfig?.code}{" "}
552 <span className="text-muted-foreground">
553 {regionConfig?.location}
554 </span>
555 </TableCell>
556 </TableRow>
557 <TableRow className="[&>:not(:last-child)]:border-r">
558 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
559 Cloud Provider
560 </TableHead>
561 <TableCell className="inline-flex max-w-full overflow-x-auto whitespace-normal font-mono">
562 <IconCloudProvider
563 provider={regionConfig?.provider}
564 className="mt-0.5"
565 />
566 <span className="ml-1 text-muted-foreground">
567 {regionConfig?.provider}
568 </span>
569 </TableCell>
570 </TableRow>
571 {data.trigger ? (
572 <TableRow className="[&>:not(:last-child)]:border-r">
573 <TableHead className="bg-muted/50 font-normal text-muted-foreground">
574 Trigger
575 </TableHead>
576 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
577 {data?.trigger}
578 </TableCell>
579 </TableRow>
580 ) : null}
581 {data?.records ? (
582 <>
583 <TableRow>
584 <TableHead colSpan={2}>Records</TableHead>
585 </TableRow>
586 <TableRow className="hover:bg-transparent">
587 <TableCell colSpan={2} className="p-0">
588 <Tabs defaultValue="table" className="w-full gap-0">
589 <TabsList className="w-full justify-start rounded-none border-b px-2">
590 <TabsTrigger value="table">
591 <TableProperties className="size-3 rotate-180" />
592 </TabsTrigger>
593 <TabsTrigger value="raw">
594 <Braces className="size-3" />
595 </TabsTrigger>
596 </TabsList>
597 <TabsContent value="table">
598 <Table className="table-fixed">
599 <colgroup>
600 <col className="w-1/3" />
601 <col className="w-2/3" />
602 </colgroup>
603 <TableBody>
604 {Object.entries(data?.records ?? {}).map(
605 ([key, value]) => (
606 <TableRow
607 key={key}
608 className="[&>:not(:last-child)]:border-r"
609 >
610 <TableHead className="overflow-x-auto bg-muted/50 font-normal text-muted-foreground">
611 {key.toUpperCase()}
612 </TableHead>
613 <TableCell className="max-w-full overflow-x-auto whitespace-normal font-mono">
614 {Array.isArray(value)
615 ? value.join(", ")
616 : value}
617 </TableCell>
618 </TableRow>
619 ),
620 )}
621 </TableBody>
622 </Table>
623 </TabsContent>
624 <TabsContent value="raw">
625 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-4 font-mono text-sm">
626 {JSON.stringify(data?.records, null, 2)}
627 </pre>
628 </TabsContent>
629 </Tabs>
630 </TableCell>
631 </TableRow>
632 </>
633 ) : null}
634 {data?.errorMessage ? (
635 <>
636 <TableRow>
637 <TableHead colSpan={2}>Error Message</TableHead>
638 </TableRow>
639 <TableRow>
640 <TableCell colSpan={2} className="p-0">
641 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm">
642 {data.errorMessage}
643 </pre>
644 </TableCell>
645 </TableRow>
646 </>
647 ) : null}
648 {data.assertions ? (
649 <>
650 <TableRow>
651 <TableHead colSpan={2}>Assertions</TableHead>
652 </TableRow>
653 <TableRow>
654 <TableCell colSpan={2} className="p-0">
655 {!data.assertions || data.assertions === "[]" ? (
656 <div className="p-2 font-mono text-muted-foreground text-sm">
657 No assertions
658 </div>
659 ) : (
660 <pre className="max-w-full overflow-x-auto whitespace-pre-wrap rounded-none bg-muted/50 p-2 font-mono text-sm">
661 {JSON.stringify(data.assertions, null, 2)}
662 </pre>
663 )}
664 </TableCell>
665 </TableRow>
666 </>
667 ) : null}
668 </TableBody>
669 </Table>
670 );
671}