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