Openstatus www.openstatus.dev
at main 671 lines 26 kB view raw
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}