Openstatus
www.openstatus.dev
1"use client";
2
3import { HoverCardTimestamp } from "@/components/common/hover-card-timestamp";
4import { TableCellDate } from "@/components/data-table/table-cell-date";
5import { TableCellNumber } from "@/components/data-table/table-cell-number";
6import {
7 HoverCard,
8 HoverCardContent,
9 HoverCardTrigger,
10} from "@/components/ui/hover-card";
11import {
12 Tooltip,
13 TooltipContent,
14 TooltipProvider,
15 TooltipTrigger,
16} from "@/components/ui/tooltip";
17import { getStatusCodeVariant, textColors } from "@/data/status-codes";
18import { cn } from "@/lib/utils";
19import type { RouterOutputs } from "@openstatus/api";
20import type { PrivateLocation } from "@openstatus/db/src/schema";
21import { getRegionInfo } from "@openstatus/regions";
22import { HoverCardPortal } from "@radix-ui/react-hover-card";
23import type { ColumnDef } from "@tanstack/react-table";
24import { Clock, Workflow } from "lucide-react";
25
26type ResponseLog = RouterOutputs["tinybird"]["list"]["data"][number];
27
28// export const columns: ColumnDef<ResponseLog>[] =
29export function getColumns(
30 privateLocations: PrivateLocation[],
31): ColumnDef<ResponseLog>[] {
32 return [
33 {
34 accessorKey: "requestStatus",
35 header: () => null,
36 enableSorting: false,
37 enableHiding: false,
38 cell: ({ row }) => {
39 const value = row.getValue("requestStatus");
40 if (value === "error") {
41 return <div className="h-2.5 w-2.5 rounded-[2px] bg-destructive" />;
42 }
43 if (value === "degraded") {
44 return <div className="h-2.5 w-2.5 rounded-[2px] bg-warning" />;
45 }
46 if (value === "success") {
47 return <div className="h-2.5 w-2.5 rounded-[2px] bg-success" />;
48 }
49 return <div className="text-muted-foreground">-</div>;
50 },
51 },
52 {
53 accessorKey: "timestamp",
54 header: "Timestamp",
55 enableSorting: false,
56 enableHiding: false,
57 cell: ({ row }) => {
58 const value = new Date(row.getValue("timestamp"));
59 return (
60 <HoverCardTimestamp date={value}>
61 <TableCellDate
62 value={value}
63 className="font-mono text-foreground"
64 />
65 </HoverCardTimestamp>
66 );
67 },
68 },
69 {
70 accessorKey: "statusCode",
71 header: "Status",
72 enableSorting: false,
73 enableHiding: false,
74 cell: ({ row }) => {
75 const log = row.original;
76 if (log.type === "http") {
77 const value = log.statusCode;
78 const variant = getStatusCodeVariant(value);
79 return (
80 <TableCellNumber value={value} className={textColors[variant]} />
81 );
82 }
83 return <div className="text-muted-foreground">-</div>;
84 },
85 },
86 {
87 accessorKey: "latency",
88 header: "Latency",
89 enableSorting: false,
90 enableHiding: false,
91 cell: ({ row }) => {
92 return <TableCellNumber value={row.getValue("latency")} unit="ms" />;
93 },
94 },
95 {
96 accessorKey: "region",
97 header: "Region",
98 cell: ({ row }) => {
99 const value = row.getValue("region");
100
101 if (typeof value !== "string") {
102 return <div className="text-muted-foreground">-</div>;
103 }
104
105 const regionConfig = getRegionInfo(value, {
106 location: privateLocations.find(
107 (location) => String(location.id) === String(value),
108 )?.name,
109 });
110
111 return (
112 <div>
113 {regionConfig.location}{" "}
114 <span className="text-muted-foreground/70 text-xs">
115 ({regionConfig.provider})
116 </span>
117 </div>
118 );
119 },
120 enableSorting: false,
121 enableHiding: false,
122 filterFn: "arrIncludesSome",
123 meta: {
124 cellClassName: "text-muted-foreground font-mono",
125 },
126 },
127 {
128 accessorKey: "timing",
129 header: "Timing",
130 cell: ({ row }) => {
131 const log = row.original;
132 if (log.type === "http" && log.timing) {
133 return <HoverCardTiming timing={log.timing} latency={log.latency} />;
134 }
135 return <div className="text-muted-foreground">-</div>;
136 },
137 enableSorting: false,
138 enableHiding: false,
139 },
140 {
141 accessorKey: "trigger",
142 header: "Trigger",
143 cell: ({ row }) => {
144 const value = row.getValue("trigger");
145 if (value === "cron" || value === "api") {
146 const Icon = value === "cron" ? Clock : Workflow;
147 const label = value === "cron" ? "Scheduled" : "API";
148 return (
149 <TooltipProvider>
150 <Tooltip>
151 <TooltipTrigger>
152 <Icon className="size-3 text-muted-foreground" />
153 </TooltipTrigger>
154 <TooltipContent side="right">
155 <p>{label}</p>
156 </TooltipContent>
157 </Tooltip>
158 </TooltipProvider>
159 );
160 }
161 return <div className="text-muted-foreground">-</div>;
162 },
163 enableSorting: false,
164 enableHiding: false,
165 meta: {
166 cellClassName: "font-mono",
167 headerClassName: "sr-only",
168 },
169 },
170 ];
171}
172
173function HoverCardTiming({
174 timing,
175 latency,
176}: {
177 timing: NonNullable<Extract<ResponseLog, { type: "http" }>["timing"]>;
178 latency: number;
179}) {
180 return (
181 <HoverCard openDelay={50} closeDelay={50}>
182 <HoverCardTrigger
183 className="opacity-70 hover:opacity-100 data-[state=open]:opacity-100"
184 asChild
185 >
186 <div className="flex">
187 {Object.entries(timing).map(([key, value], index) => (
188 <div
189 key={key}
190 className={cn("h-4")}
191 style={{
192 width: `${(value / latency) * 100}%`,
193 backgroundColor: `var(--chart-${index + 1})`,
194 }}
195 />
196 ))}
197 </div>
198 </HoverCardTrigger>
199 {/* REMINDER: allows us to port the content to the document.body, which is helpful when using opacity-50 on the row element */}
200 <HoverCardPortal>
201 <HoverCardContent side="bottom" align="end" className="z-10 w-auto p-2">
202 <HoverCardTimingContent {...{ latency, timing }} />
203 </HoverCardContent>
204 </HoverCardPortal>
205 </HoverCard>
206 );
207}
208
209function HoverCardTimingContent({
210 timing,
211 latency,
212}: {
213 timing: NonNullable<Extract<ResponseLog, { type: "http" }>["timing"]>;
214 latency: number;
215}) {
216 return (
217 <div className="flex flex-col gap-1">
218 {Object.entries(timing).map(([key, value], index) => {
219 return (
220 <div key={key} className="grid grid-cols-2 gap-4 text-xs">
221 <div className="flex items-center gap-2">
222 <div
223 className={cn("h-2 w-2 rounded-full")}
224 style={{ backgroundColor: `var(--chart-${index + 1})` }}
225 />
226 <div className="font-mono text-accent-foreground uppercase">
227 {key}
228 </div>
229 </div>
230 <div className="flex items-center justify-between gap-4">
231 <div className="font-mono text-muted-foreground">
232 {`${new Intl.NumberFormat("en-US", {
233 maximumFractionDigits: 2,
234 }).format((value / latency) * 100)}%`}
235 </div>
236 <div className="font-mono">
237 {new Intl.NumberFormat("en-US", {
238 maximumFractionDigits: 3,
239 }).format(value)}
240 <span className="text-muted-foreground">ms</span>
241 </div>
242 </div>
243 </div>
244 );
245 })}
246 </div>
247 );
248}