Openstatus www.openstatus.dev

chore: improve speed checker (#1422)

* chore: improve speed checker

* chore: seo content

* refactor: icon cloud provider tooltip

authored by

Maximilian Kaske and committed by
GitHub
182bdcce 0cc30b63

+376 -144
+21
apps/dashboard/src/components/common/icon-cloud-provider.tsx
··· 1 + import { 2 + Tooltip, 3 + TooltipContent, 4 + TooltipProvider, 5 + TooltipTrigger, 6 + } from "@/components/ui/tooltip"; 1 7 import { cn } from "@/lib/utils"; 2 8 import { Fly, Koyeb, Railway } from "@openstatus/icons"; 3 9 import { Globe } from "lucide-react"; ··· 19 25 return <Globe className={cn("size-4", className)} />; 20 26 } 21 27 } 28 + 29 + export function IconCloudProviderTooltip( 30 + props: React.ComponentProps<typeof IconCloudProvider>, 31 + ) { 32 + return ( 33 + <TooltipProvider> 34 + <Tooltip delayDuration={0}> 35 + <TooltipTrigger type="button"> 36 + <IconCloudProvider {...props} /> 37 + </TooltipTrigger> 38 + <TooltipContent className="capitalize">{props.provider}</TooltipContent> 39 + </Tooltip> 40 + </TooltipProvider> 41 + ); 42 + }
+5 -20
apps/dashboard/src/components/forms/monitor/form-scheduling-regions.tsx
··· 33 33 import { toast } from "sonner"; 34 34 import { z } from "zod"; 35 35 36 - import { IconCloudProvider } from "@/components/common/icon-cloud-provider"; 36 + import { IconCloudProviderTooltip } from "@/components/common/icon-cloud-provider"; 37 37 import { Note, NoteButton } from "@/components/common/note"; 38 38 import { UpgradeDialog } from "@/components/dialogs/upgrade"; 39 - import { 40 - Tooltip, 41 - TooltipContent, 42 - TooltipProvider, 43 - TooltipTrigger, 44 - } from "@/components/ui/tooltip"; 45 39 import { useTRPC } from "@/lib/trpc/client"; 46 40 import { groupByContinent } from "@openstatus/utils"; 47 41 import { useQuery } from "@tanstack/react-query"; ··· 313 307 <span className="truncate font-normal text-muted-foreground text-xs leading-[inherit]"> 314 308 {region.location} 315 309 </span> 316 - <TooltipProvider> 317 - <Tooltip> 318 - <TooltipTrigger type="button"> 319 - <IconCloudProvider 320 - provider={region.provider} 321 - className="size-3" 322 - /> 323 - </TooltipTrigger> 324 - <TooltipContent className="capitalize"> 325 - {region.provider} 326 - </TooltipContent> 327 - </Tooltip> 328 - </TooltipProvider> 310 + <IconCloudProviderTooltip 311 + provider={region.provider} 312 + className="size-3" 313 + /> 329 314 </FormLabel> 330 315 </FormItem> 331 316 );
+1 -1
apps/web/src/app/(pages)/(content)/play/checker/[id]/page.tsx
··· 78 78 const params = await props.params; 79 79 const title = "Global Speed Checker"; 80 80 const description = 81 - "Get speed insights for your api, website from multiple regions."; 81 + "API speed test and website speed checker: global latency speed test from different locations."; 82 82 return { 83 83 ...defaultMetadata, 84 84 title,
+24 -63
apps/web/src/app/(pages)/(content)/play/checker/_components/checker-form.tsx
··· 8 8 9 9 import { 10 10 Button, 11 - Checkbox, 12 11 Form, 13 12 FormControl, 14 13 FormDescription, ··· 34 33 TooltipTrigger, 35 34 } from "@openstatus/ui"; 36 35 36 + import { IconCloudProviderTooltip } from "@/components/icon-cloud-provider"; 37 37 import { Icons } from "@/components/icons"; 38 38 import { LoadingAnimation } from "@/components/loading-animation"; 39 39 import { ··· 46 46 import { toast } from "@/lib/toast"; 47 47 import { notEmpty } from "@/lib/utils"; 48 48 import { monitorRegions } from "@openstatus/db/src/schema/constants"; 49 - import { Fly, Koyeb, Railway } from "@openstatus/icons"; 50 49 import { regionDict } from "@openstatus/utils"; 51 - import { 52 - ArrowRight, 53 - ChevronRight, 54 - Gauge, 55 - Globe, 56 - Info, 57 - Loader, 58 - } from "lucide-react"; 50 + import { ArrowRight, ChevronRight, Gauge, Info, Loader } from "lucide-react"; 59 51 import dynamic from "next/dynamic"; 60 52 import Link from "next/link"; 61 53 import { useQueryStates } from "nuqs"; ··· 150 142 setSearchParams({ id: item }); 151 143 toast.success("Data is available!", { 152 144 id: toastId, 153 - duration: 3000, 154 - description: "Click the button below to more.", 145 + description: "Learn about the response details.", 155 146 action: { 156 147 label: "Details", 157 148 onClick: () => ··· 199 190 toast.error("Something went wrong", { 200 191 description: "Please try again", 201 192 id: toastId, 202 - duration: 2000, 203 193 }); 194 + } finally { 195 + if (toastId) { 196 + setTimeout(() => toast.dismiss(toastId), 3000); 197 + } 204 198 } 205 199 } 206 200 ··· 273 267 )} 274 268 </Button> 275 269 </div> 276 - </div> 277 - <div> 278 - <FormField 279 - control={form.control} 280 - name="redirect" 281 - render={({ field }) => ( 282 - <FormItem className="flex flex-row items-start space-x-2 space-y-0"> 283 - <FormControl> 284 - <Checkbox 285 - checked={field.value} 286 - onCheckedChange={field.onChange} 287 - /> 288 - </FormControl> 289 - <div className="space-y-1 leading-none"> 290 - <FormLabel>Redirect to extended details</FormLabel> 291 - <FormDescription className="max-w-md"> 292 - Get response header, timing phases and more. 293 - </FormDescription> 294 - </div> 295 - </FormItem> 296 - )} 297 - /> 270 + <div className="-mt-1 col-span-full"> 271 + <FormDescription className="text-xs"> 272 + Access the response header, timing phases and latency. 273 + </FormDescription> 274 + </div> 298 275 </div> 299 276 </form> 300 277 </Form> ··· 382 359 return ( 383 360 <TableRow key={item.region}> 384 361 <TableCell className="flex items-center gap-2 font-medium"> 385 - <TooltipProvider> 386 - <Tooltip delayDuration={0}> 387 - <TooltipTrigger type="button"> 388 - {(() => { 389 - switch (r.provider) { 390 - case "fly": 391 - return <Fly className="size-4" />; 392 - case "railway": 393 - return <Railway className="size-4" />; 394 - case "koyeb": 395 - return <Koyeb className="size-4" />; 396 - default: 397 - return <Globe className="size-4" />; 398 - } 399 - })()} 400 - </TooltipTrigger> 401 - <TooltipContent className="capitalize"> 402 - {r.provider} 403 - </TooltipContent> 404 - </Tooltip> 405 - </TooltipProvider> 362 + <IconCloudProviderTooltip provider={r.provider} /> 406 363 {region} 407 364 <StatusDot value={item.status} /> 408 365 </TableCell> 409 - <TableCell className="text-right">{latency}</TableCell> 366 + <TableCell className="text-right font-mono"> 367 + {latency} 368 + </TableCell> 410 369 </TableRow> 411 370 ); 412 371 }) ··· 444 403 function StatusDot({ value }: { value: number }) { 445 404 switch (String(value).charAt(0)) { 446 405 case "1": 447 - return <div className="h-1.5 w-1.5 rounded-full bg-gray-500" />; 406 + return <div className="h-1.5 w-1.5 shrink-0 rounded-full bg-gray-500" />; 448 407 case "2": 449 - return <div className="h-1.5 w-1.5 rounded-full bg-green-500" />; 408 + return <div className="h-1.5 w-1.5 shrink-0 rounded-full bg-green-500" />; 450 409 case "3": 451 - return <div className="h-1.5 w-1.5 rounded-full bg-blue-500" />; 410 + return <div className="h-1.5 w-1.5 shrink-0 rounded-full bg-blue-500" />; 452 411 case "4": 453 - return <div className="h-1.5 w-1.5 rounded-full bg-yellow-500" />; 412 + return ( 413 + <div className="h-1.5 w-1.5 shrink-0 rounded-full bg-yellow-500" /> 414 + ); 454 415 case "5": 455 - return <div className="h-1.5 w-1.5 rounded-full bg-red-500" />; 416 + return <div className="h-1.5 w-1.5 shrink-0 rounded-full bg-red-500" />; 456 417 default: 457 - return <div className="h-1.5 w-1.5 rounded-full bg-gray-500" />; 418 + return <div className="h-1.5 w-1.5 shrink-0 rounded-full bg-gray-500" />; 458 419 } 459 420 }
+4 -2
apps/web/src/app/(pages)/(content)/play/checker/_components/global-monitoring.tsx
··· 10 10 CardIcon, 11 11 CardTitle, 12 12 } from "@/components/marketing/card"; 13 + import { regionDict } from "@openstatus/utils"; 14 + 15 + const TOTAL_REGIONS = Object.keys(regionDict).length; 13 16 14 17 const features: { 15 18 icon: ValidIcon; ··· 25 28 { 26 29 icon: "globe", 27 30 catchline: "Global Speed Test", 28 - description: 29 - "Monitor latency performance in different regions to ensure quick load times for users across 35 regions worldwide.", 31 + description: `Monitor latency performance in different regions to ensure quick load times for users across ${TOTAL_REGIONS} regions worldwide.`, 30 32 }, 31 33 { 32 34 icon: "link",
+85
apps/web/src/app/(pages)/(content)/play/checker/_components/informations.tsx
··· 1 + import { Shell } from "@/components/dashboard/shell"; 2 + import { Separator } from "@openstatus/ui"; 3 + import { regionDict } from "@openstatus/utils"; 4 + 5 + const TOTAL_REGIONS = Object.keys(regionDict).length; 6 + const TOTAL_PROVIDERS = Object.keys(regionDict).reduce((acc, region) => { 7 + return acc.add(regionDict[region as keyof typeof regionDict].provider); 8 + }, new Set<"fly" | "koyeb" | "railway">()); 9 + 10 + export function Informations() { 11 + return ( 12 + <Shell> 13 + <div className="grid gap-4"> 14 + <div className="grid gap-1"> 15 + <h3 className="font-semibold">What Is a Website Speed Checker?</h3> 16 + <p className="text-muted-foreground"> 17 + A Website Speed Checker is an online tool that measures how fast 18 + your website or API responds when someone visits it. It analyzes 19 + various website performance metrics to help you understand which 20 + elements slow down your page load time. 21 + </p> 22 + <p className="text-muted-foreground"> 23 + Speed checkers can focus on two aspects of performance: 24 + </p> 25 + <ul className="ml-4 list-outside list-disc space-y-1 text-muted-foreground"> 26 + <li> 27 + <strong className="font-semibold text-foreground"> 28 + Client-side performance 29 + </strong> 30 + , which includes metrics like First Contentful Paint (FCP), 31 + Largest Contentful Paint (LCP), and Cumulative Layout Shift (CLS) 32 + — all indicators of how quickly your site becomes visible and 33 + usable to visitors. 34 + </li> 35 + <li> 36 + <strong className="font-semibold text-foreground"> 37 + Server-side performance (or network performance) 38 + </strong> 39 + , which looks at the technical steps of a request such as DNS 40 + lookup, TCP connection, TLS handshake, and server response time. 41 + </li> 42 + </ul> 43 + <p className="text-muted-foreground"> 44 + Understanding both sides helps you identify whether slowdowns are 45 + caused by your frontend assets or your backend infrastructure. 46 + </p> 47 + </div> 48 + <div className="grid gap-1"> 49 + <h3 className="font-semibold">What Is a Global Speed Checker?</h3> 50 + <p className="text-muted-foreground"> 51 + A Global Speed Checker measures your website or API's latency and 52 + response time from multiple locations around the world. Instead of 53 + testing from just one data center, it runs checks from{" "} 54 + {TOTAL_REGIONS} global regions across {TOTAL_PROVIDERS.size} cloud 55 + providers, giving you a complete picture of your site's real-world 56 + performance. 57 + </p> 58 + </div> 59 + <Separator /> 60 + <div className="grid gap-1"> 61 + <p className="text-muted-foreground">With OpenStatus, you can:</p> 62 + <ul className="ml-4 list-outside list-disc space-y-1 text-muted-foreground"> 63 + <li>Test how fast your API or website responds worldwide.</li> 64 + <li>Compare latency across different regions.</li> 65 + <li>Identify network bottlenecks.</li> 66 + <li>Monitor uptime and availability in real time.</li> 67 + </ul> 68 + <p className="text-muted-foreground"> 69 + Whether you want to test your website speed from Europe, Asia, North 70 + America, or beyond, our Global Speed Checker gives accurate, 71 + consistent data from distributed locations. 72 + </p> 73 + <p className="text-muted-foreground"> 74 + If you'd like to request additional test regions or providers, feel 75 + free to contact us at{" "} 76 + <a href="mailto:ping@openstatus.dev" className="text-foreground"> 77 + ping@openstatus.dev 78 + </a> 79 + . 80 + </p> 81 + </div> 82 + </div> 83 + </Shell> 84 + ); 85 + }
+3 -1
apps/web/src/app/(pages)/(content)/play/checker/layout.tsx
··· 21 21 </BreadcrumbItem> 22 22 <BreadcrumbSeparator /> 23 23 <BreadcrumbItem> 24 - <BreadcrumbPage>Checker</BreadcrumbPage> 24 + <BreadcrumbPage> 25 + <Link href="/play/checker">Checker</Link> 26 + </BreadcrumbPage> 25 27 </BreadcrumbItem> 26 28 </BreadcrumbList> 27 29 </Breadcrumb>
+4 -2
apps/web/src/app/(pages)/(content)/play/checker/page.tsx
··· 10 10 import { redirect } from "next/navigation"; 11 11 import CheckerPlay from "./_components/checker-play"; 12 12 import { GlobalMonitoring } from "./_components/global-monitoring"; 13 + import { Informations } from "./_components/informations"; 13 14 import { Testimonial } from "./_components/testimonial"; 14 15 import { mockCheckAllRegions } from "./api/mock"; 15 16 import { searchParamsCache } from "./search-params"; 16 17 17 18 const TITLE = "Global Speed Checker"; 18 19 const DESCRIPTION = 19 - "API speed test and website speed checker: global speed test from different locations. Free speed test."; 20 + "API speed test and website speed checker: global latency speed test from different locations."; 20 21 21 22 const OG_DESCRIPTION = 22 - "Test the performance of your api, website from different locations. Get speed insights for free."; 23 + "Test the performance of your api and website from different locations."; 23 24 24 25 export const metadata: Metadata = { 25 26 ...defaultMetadata, ··· 58 59 <CheckerPlay data={data} /> 59 60 <Testimonial /> 60 61 <GlobalMonitoring /> 62 + <Informations /> 61 63 <BottomCTA className="mx-auto max-w-2xl lg:max-w-4xl" /> 62 64 </div> 63 65 );
+42
apps/web/src/components/icon-cloud-provider.tsx
··· 1 + import { cn } from "@/lib/utils"; 2 + import { Fly, Koyeb, Railway } from "@openstatus/icons"; 3 + import { 4 + Tooltip, 5 + TooltipContent, 6 + TooltipProvider, 7 + TooltipTrigger, 8 + } from "@openstatus/ui"; 9 + import { Globe } from "lucide-react"; 10 + 11 + export function IconCloudProvider({ 12 + provider, 13 + className, 14 + }: React.ComponentProps<"svg"> & { 15 + provider: string; 16 + }) { 17 + switch (provider) { 18 + case "fly": 19 + return <Fly className={cn("size-4", className)} />; 20 + case "koyeb": 21 + return <Koyeb className={cn("size-4", className)} />; 22 + case "railway": 23 + return <Railway className={cn("size-4", className)} />; 24 + default: 25 + return <Globe className={cn("size-4", className)} />; 26 + } 27 + } 28 + 29 + export function IconCloudProviderTooltip( 30 + props: React.ComponentProps<typeof IconCloudProvider>, 31 + ) { 32 + return ( 33 + <TooltipProvider> 34 + <Tooltip delayDuration={0}> 35 + <TooltipTrigger type="button"> 36 + <IconCloudProvider {...props} /> 37 + </TooltipTrigger> 38 + <TooltipContent className="capitalize">{props.provider}</TooltipContent> 39 + </Tooltip> 40 + </TooltipProvider> 41 + ); 42 + }
+3 -14
apps/web/src/components/monitor-dashboard/region-preset.tsx
··· 1 1 "use client"; 2 2 3 - import { Check, ChevronsUpDown, Globe, Globe2 } from "lucide-react"; 3 + import { Check, ChevronsUpDown, Globe2 } from "lucide-react"; 4 4 5 5 import { Button, type ButtonProps } from "@openstatus/ui/src/components/button"; 6 6 import { ··· 19 19 } from "@openstatus/ui/src/components/popover"; 20 20 import { type Continent, type RegionInfo, regionDict } from "@openstatus/utils"; 21 21 22 + import { IconCloudProvider } from "@/components/icon-cloud-provider"; 22 23 import { cn } from "@/lib/utils"; 23 24 import { 24 25 type Region, 25 26 monitorRegions, 26 27 } from "@openstatus/db/src/schema/constants"; 27 - import { Fly, Koyeb, Railway } from "@openstatus/icons"; 28 28 import { parseAsArrayOf, parseAsStringLiteral, useQueryState } from "nuqs"; 29 29 30 30 interface RegionsPresetProps extends ButtonProps { ··· 139 139 </div> 140 140 <div className="flex w-full items-center gap-1"> 141 141 <span> 142 - {(() => { 143 - switch (region.provider) { 144 - case "fly": 145 - return <Fly className="size-4" />; 146 - case "railway": 147 - return <Railway className="size-4" />; 148 - case "koyeb": 149 - return <Koyeb className="size-4" />; 150 - default: 151 - return <Globe className="size-4" />; 152 - } 153 - })()} 142 + <IconCloudProvider provider={region.provider} /> 154 143 </span> 155 144 <span> 156 145 {code.replace(/(koyeb_|railway_|fly_)/g, "")}{" "}
+140 -36
apps/web/src/components/ping-response-analysis/columns.tsx
··· 3 3 import type { ColumnDef } from "@tanstack/react-table"; 4 4 import { type RegionChecker, latencyFormatter, regionFormatter } from "./utils"; 5 5 6 - import { Fly, Koyeb, Railway } from "@openstatus/icons"; 6 + import { DataTableColumnHeader } from "@/components/data-table/data-table-column-header"; 7 + import { IconCloudProviderTooltip } from "@/components/icon-cloud-provider"; 8 + import { cn } from "@/lib/utils"; 7 9 import { 8 - Tooltip, 9 - TooltipContent, 10 - TooltipProvider, 11 - TooltipTrigger, 10 + HoverCard, 11 + HoverCardContent, 12 + HoverCardPortal, 13 + HoverCardTrigger, 12 14 } from "@openstatus/ui"; 13 15 import { regionDict } from "@openstatus/utils"; 14 - import { Globe } from "lucide-react"; 15 - import { DataTableColumnHeader } from "../data-table/data-table-column-header"; 16 - import { StatusCodeBadge } from "../monitor/status-code-badge"; 17 16 18 17 export const columns: ColumnDef<RegionChecker>[] = [ 19 18 { ··· 24 23 const region = regionDict[row.original.region]; 25 24 return ( 26 25 <div className="flex items-center gap-1.5"> 27 - <TooltipProvider> 28 - <Tooltip delayDuration={0}> 29 - <TooltipTrigger type="button"> 30 - {(() => { 31 - switch (region.provider) { 32 - case "fly": 33 - return <Fly className="size-4" />; 34 - case "railway": 35 - return <Railway className="size-4" />; 36 - case "koyeb": 37 - return <Koyeb className="size-4" />; 38 - default: 39 - return <Globe className="size-4" />; 40 - } 41 - })()} 42 - </TooltipTrigger> 43 - <TooltipContent className="capitalize"> 44 - {region.provider} 45 - </TooltipContent> 46 - </Tooltip> 47 - </TooltipProvider> 26 + <IconCloudProviderTooltip provider={region.provider} /> 48 27 <div> 49 28 <span className="font-mono"> 50 29 {row.original.region.replace(/(koyeb_|railway_|fly_)/g, "")} ··· 60 39 const region = regionFormatter(row.original.region, "long").toLowerCase(); 61 40 const continent = 62 41 regionDict[row.original.region].continent.toLocaleLowerCase(); 63 - return `${region} ${continent}`.includes(filterValue.toLowerCase()); 42 + const provider = 43 + regionDict[row.original.region].provider.toLocaleLowerCase(); 44 + return `${region} ${continent} ${provider}`.includes( 45 + filterValue.toLowerCase(), 46 + ); 64 47 }, 65 48 }, 66 49 { 67 50 accessorKey: "status", 68 51 header: "Status", 69 52 cell: ({ row }) => { 70 - return <StatusCodeBadge statusCode={row.original.status} />; 53 + const statusCode = row.original.status; 54 + const yellow = String(statusCode).startsWith("1"); 55 + const green = String(statusCode).startsWith("2"); 56 + const blue = String(statusCode).startsWith("3"); 57 + const rose = 58 + String(statusCode).startsWith("4") || 59 + String(statusCode).startsWith("5"); 60 + return ( 61 + <div 62 + className={cn("font-mono", { 63 + "text-green-500": green, 64 + "text-blue-500": blue, 65 + "text-rose-500": rose, 66 + "text-yellow-500": yellow, 67 + })} 68 + > 69 + {statusCode} 70 + </div> 71 + ); 71 72 }, 72 73 }, 73 74 { ··· 84 85 accessorFn: (row) => `${row.timing.dnsDone - row.timing.dnsStart}`, 85 86 cell: ({ row, column }) => { 86 87 return ( 87 - <div className="text-right font-mono"> 88 + <div className="text-right font-mono text-muted-foreground"> 88 89 {latencyFormatter(row.getValue(column.id))} 89 90 </div> 90 91 ); ··· 107 108 }, 108 109 cell: ({ row, column }) => { 109 110 return ( 110 - <div className="text-right font-mono"> 111 + <div className="text-right font-mono text-muted-foreground"> 111 112 {latencyFormatter(row.getValue(column.id))} 112 113 </div> 113 114 ); ··· 131 132 }, 132 133 cell: ({ row, column }) => { 133 134 return ( 134 - <div className="text-right font-mono"> 135 + <div className="text-right font-mono text-muted-foreground"> 135 136 {latencyFormatter(row.getValue(column.id))} 136 137 </div> 137 138 ); ··· 155 156 }, 156 157 cell: ({ row, column }) => { 157 158 return ( 158 - <div className="text-right font-mono"> 159 + <div className="text-right font-mono text-muted-foreground"> 159 160 {latencyFormatter(row.getValue(column.id))} 160 161 </div> 161 162 ); ··· 179 180 }, 180 181 cell: ({ row, column }) => { 181 182 return ( 182 - <div className="text-right font-mono"> 183 + <div className="text-right font-mono text-muted-foreground"> 183 184 {latencyFormatter(row.getValue(column.id))} 184 185 </div> 185 186 ); ··· 210 211 headerClassName: "text-right", 211 212 }, 212 213 }, 214 + // { 215 + // accessorKey: "timing", 216 + // header: "Timing", 217 + // cell: ({ row }) => { 218 + // return <HoverCardTiming timing={row.original.timing} />; 219 + // }, 220 + // meta: { 221 + // headerClassName: "text-right", 222 + // }, 223 + // }, 213 224 ]; 225 + 226 + function HoverCardTiming({ 227 + timing: rawTiming, 228 + }: { 229 + timing: NonNullable<Extract<RegionChecker, { type: "http" }>["timing"]>; 230 + }) { 231 + const timing = getTiming(rawTiming); 232 + const total = getTotal(timing); 233 + return ( 234 + <HoverCard openDelay={50} closeDelay={50}> 235 + <HoverCardTrigger 236 + className="opacity-70 hover:opacity-100 data-[state=open]:opacity-100" 237 + asChild 238 + > 239 + <div className="flex"> 240 + {Object.entries(timing).map(([key, value], index) => ( 241 + <div 242 + key={key} 243 + className={cn("h-4")} 244 + style={{ 245 + width: `${(value / total) * 100}%`, 246 + backgroundColor: `hsl(var(--chart-${index + 1}))`, 247 + }} 248 + /> 249 + ))} 250 + </div> 251 + </HoverCardTrigger> 252 + {/* REMINDER: allows us to port the content to the document.body, which is helpful when using opacity-50 on the row element */} 253 + <HoverCardPortal> 254 + <HoverCardContent side="bottom" align="end" className="z-10 w-auto p-2"> 255 + <HoverCardTimingContent timing={rawTiming} /> 256 + </HoverCardContent> 257 + </HoverCardPortal> 258 + </HoverCard> 259 + ); 260 + } 261 + 262 + function HoverCardTimingContent({ 263 + timing: rawTiming, 264 + }: { 265 + timing: NonNullable<Extract<RegionChecker, { type: "http" }>["timing"]>; 266 + }) { 267 + const timing = getTiming(rawTiming); 268 + const total = getTotal(timing); 269 + return ( 270 + <div className="flex flex-col gap-1"> 271 + {Object.entries(timing).map(([key, value], index) => { 272 + return ( 273 + <div key={key} className="grid grid-cols-2 gap-4 text-xs"> 274 + <div className="flex items-center gap-2"> 275 + <div 276 + className={cn("h-2 w-2 rounded-full")} 277 + style={{ backgroundColor: `hsl(var(--chart-${index + 1}))` }} 278 + /> 279 + <div className="font-mono text-accent-foreground uppercase"> 280 + {key} 281 + </div> 282 + </div> 283 + <div className="flex items-center justify-between gap-4"> 284 + <div className="font-mono text-muted-foreground"> 285 + {`${new Intl.NumberFormat("en-US", { 286 + maximumFractionDigits: 2, 287 + }).format((value / total) * 100)}%`} 288 + </div> 289 + <div className="font-mono"> 290 + {new Intl.NumberFormat("en-US", { 291 + maximumFractionDigits: 3, 292 + }).format(value)} 293 + <span className="text-muted-foreground">ms</span> 294 + </div> 295 + </div> 296 + </div> 297 + ); 298 + })} 299 + </div> 300 + ); 301 + } 302 + 303 + function getTotal(timing: ReturnType<typeof getTiming>) { 304 + return Object.values(timing).reduce((acc, curr) => acc + curr, 0); 305 + } 306 + 307 + function getTiming( 308 + timing: NonNullable<Extract<RegionChecker, { type: "http" }>["timing"]>, 309 + ) { 310 + return { 311 + dns: timing.dnsDone - timing.dnsStart, 312 + connect: timing.connectDone - timing.connectStart, 313 + tls: timing.tlsHandshakeDone - timing.tlsHandshakeStart, 314 + ttfb: timing.firstByteDone - timing.firstByteStart, 315 + transfer: timing.transferDone - timing.transferStart, 316 + }; 317 + }
+36 -1
apps/web/src/components/ping-response-analysis/multi-region-chart.tsx
··· 5 5 6 6 import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; 7 7 8 + import { IconCloudProvider } from "@/components/icon-cloud-provider"; 8 9 import { 9 10 type ChartConfig, 10 11 ChartContainer, ··· 13 14 ChartTooltip, 14 15 ChartTooltipContent, 15 16 } from "@openstatus/ui/src/components/chart"; 17 + import { regionDict } from "@openstatus/utils"; 16 18 17 19 const chartConfig = { 18 20 dns: { ··· 84 86 }} 85 87 /> 86 88 <ChartTooltip 89 + defaultIndex={5} 87 90 content={ 88 91 <ChartTooltipContent 89 - labelFormatter={(label) => regionFormatter(label, "long")} 92 + labelFormatter={(label) => { 93 + return ( 94 + <div className="flex items-center gap-2"> 95 + <IconCloudProvider 96 + provider={ 97 + regionDict[label as keyof typeof regionDict].provider 98 + } 99 + className="size-2.5!" 100 + /> 101 + {regionFormatter(label, "long")} 102 + </div> 103 + ); 104 + }} 105 + formatter={(value, name) => ( 106 + <> 107 + <div 108 + className="h-2.5 w-2.5 shrink-0 rounded-[2px] bg-(--color-bg)" 109 + style={ 110 + { 111 + "--color-bg": `var(--color-${name})`, 112 + } as React.CSSProperties 113 + } 114 + /> 115 + {chartConfig[name as keyof typeof chartConfig]?.label || 116 + name} 117 + <div className="ml-auto flex items-baseline gap-0.5 font-medium font-mono text-foreground tabular-nums"> 118 + {value} 119 + <span className="font-normal text-muted-foreground"> 120 + ms 121 + </span> 122 + </div> 123 + </> 124 + )} 90 125 /> 91 126 } 92 127 />
+8 -4
apps/web/src/components/ping-response-analysis/multi-region-table.tsx
··· 37 37 renderSubComponent(props: { row: Row<TData> }): React.ReactElement<unknown>; 38 38 getRowCanExpand(row: Row<TData>): boolean; 39 39 autoResetExpanded?: boolean; 40 + defaultColumnVisibility?: VisibilityState; 40 41 } 41 42 42 43 export function MultiRegionTable<TData, TValue>({ ··· 45 46 renderSubComponent, 46 47 getRowCanExpand, 47 48 autoResetExpanded, 49 + defaultColumnVisibility, 48 50 }: DataTableProps<TData, TValue>) { 49 51 const [sorting, setSorting] = useState<SortingState>([ 50 52 { id: "latency", desc: false }, 51 53 ]); 52 54 const [expanded, setExpanded] = useState<ExpandedState>({}); 53 - const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({}); 55 + const [columnVisibility, setColumnVisibility] = useState<VisibilityState>( 56 + defaultColumnVisibility ?? {}, 57 + ); 54 58 const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]); 55 59 56 60 const table = useReactTable({ ··· 78 82 <div className="grid gap-4"> 79 83 <div className="flex items-end justify-between gap-2"> 80 84 <Input 81 - placeholder="Filter regions, continents, flags..." 85 + placeholder="Filter region, continent, flag, cloud provider..." 82 86 value={(table.getColumn("region")?.getFilterValue() as string) ?? ""} 83 87 onChange={(event) => 84 88 table.getColumn("region")?.setFilterValue(event.target.value) 85 89 } 86 - className="h-8 max-w-[325px] truncate" 90 + className="h-8 max-w-[350px] truncate" 87 91 /> 88 92 <div className="flex items-center justify-end gap-2"> 89 93 <DataTableCollapseButton table={table} /> ··· 91 95 </div> 92 96 </div> 93 97 <Table> 94 - <TableCaption>Multi Regions</TableCaption> 98 + <TableCaption>Multi Cloud Regions</TableCaption> 95 99 <TableHeader className="bg-muted/50"> 96 100 {table.getHeaderGroups().map((headerGroup) => ( 97 101 <TableRow key={headerGroup.id}>