Openstatus www.openstatus.dev

feat: add tb data to overview list (#685)

authored by

Maximilian Kaske and committed by
GitHub
a016bdf8 21bf9b4e

+137 -42
+29 -1
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/(overview)/page.tsx
··· 7 7 import { Limit } from "@/components/dashboard/limit"; 8 8 import { columns } from "@/components/data-table/monitor/columns"; 9 9 import { DataTable } from "@/components/data-table/monitor/data-table"; 10 + import { getMonitorListData, getResponseTimeMetricsData } from "@/lib/tb"; 11 + import { convertTimezoneToGMT } from "@/lib/timezone"; 10 12 import { api } from "@/trpc/server"; 11 13 12 14 export default async function MonitorPage() { ··· 27 29 /> 28 30 ); 29 31 32 + const gmt = convertTimezoneToGMT(); 33 + 34 + // maybe not very efficient? 35 + // use Suspense and Client call instead? 36 + const monitorsWithData = await Promise.all( 37 + monitors.map(async (monitor) => { 38 + const metrics = await getResponseTimeMetricsData({ 39 + monitorId: String(monitor.id), 40 + url: monitor.url, 41 + interval: 24, 42 + }); 43 + 44 + const tracker = await getMonitorListData({ 45 + monitorId: String(monitor.id), 46 + url: monitor.url, 47 + timezone: gmt, 48 + }); 49 + 50 + const [current, _] = metrics 51 + ? metrics.sort((a, b) => (a.time - b.time < 0 ? 1 : -1)) 52 + : [undefined]; 53 + 54 + return { monitor, metrics: current, tracker }; 55 + }), 56 + ); 57 + 30 58 return ( 31 59 <> 32 - <DataTable columns={columns} data={monitors} /> 60 + <DataTable columns={columns} data={monitorsWithData} /> 33 61 {isLimitReached ? <Limit /> : null} 34 62 </> 35 63 );
+86 -33
apps/web/src/components/data-table/monitor/columns.tsx
··· 2 2 3 3 import Link from "next/link"; 4 4 import type { ColumnDef } from "@tanstack/react-table"; 5 + import { formatDistanceToNow } from "date-fns"; 5 6 6 7 import type { Monitor } from "@openstatus/db/src/schema"; 7 - import { Badge } from "@openstatus/ui"; 8 + import type { 9 + Monitor as MonitorTracker, 10 + ResponseTimeMetrics, 11 + } from "@openstatus/tinybird"; 12 + import { 13 + Tooltip, 14 + TooltipContent, 15 + TooltipProvider, 16 + TooltipTrigger, 17 + } from "@openstatus/ui"; 8 18 9 19 import { StatusDot } from "@/components/monitor/status-dot"; 20 + import { Bar } from "@/components/tracker/tracker"; 10 21 import { DataTableRowActions } from "./data-table-row-actions"; 11 22 12 - export const columns: ColumnDef<Monitor>[] = [ 23 + export const columns: ColumnDef<{ 24 + monitor: Monitor; 25 + metrics?: ResponseTimeMetrics; 26 + tracker?: MonitorTracker[]; 27 + }>[] = [ 13 28 { 14 29 accessorKey: "name", 15 30 header: "Name", 16 31 cell: ({ row }) => { 17 - const { active, status } = row.original; 32 + const { active, status, name } = row.original.monitor; 18 33 return ( 19 34 <Link 20 - href={`./monitors/${row.original.id}/overview`} 21 - className="group flex items-center gap-2" 35 + href={`./monitors/${row.original.monitor.id}/overview`} 36 + className="group flex max-w-[150px] items-center gap-2 md:max-w-[250px]" 22 37 > 23 38 <StatusDot active={active} status={status} /> 24 - <span className="max-w-[125px] truncate group-hover:underline"> 25 - {row.getValue("name")} 26 - </span> 39 + <span className="truncate group-hover:underline">{name}</span> 27 40 </Link> 28 41 ); 29 42 }, 30 43 }, 31 44 { 32 - accessorKey: "url", 33 - header: "URL", 45 + accessorKey: "tracker", 46 + header: "Last 7 days", 34 47 cell: ({ row }) => { 48 + const tracker = row.original.tracker?.slice(0, 7).reverse(); 35 49 return ( 36 - <div className="flex"> 37 - <span className="max-w-[150px] truncate font-medium sm:max-w-[200px] lg:max-w-[250px] xl:max-w-[350px]"> 38 - {row.getValue("url")} 39 - </span> 50 + <div className="flex w-24 gap-1"> 51 + {tracker?.map((tracker) => ( 52 + <Bar key={tracker.day} className="h-5" {...tracker} /> 53 + ))} 40 54 </div> 41 55 ); 42 56 }, 43 57 }, 44 58 { 45 - accessorKey: "description", 46 - header: "Description", 59 + accessorKey: "lastTimestamp", 60 + header: "Last ping", 61 + cell: ({ row }) => { 62 + const timestamp = row.original.metrics?.lastTimestamp; 63 + if (timestamp) { 64 + const distance = formatDistanceToNow(new Date(timestamp)); 65 + return ( 66 + <Number 67 + value={parseInt(distance.split(" ")[0])} 68 + suffix={`${distance.split(" ")[1]} ago`} 69 + /> 70 + ); 71 + } 72 + return <span className="text-muted-foreground">-</span>; 73 + }, 74 + }, 75 + { 76 + accessorKey: "uptime", 77 + header: () => <HeaderTooltip>Uptime</HeaderTooltip>, 47 78 cell: ({ row }) => { 48 - return ( 49 - <div className="flex"> 50 - <span className="text-muted-foreground max-w-[150px] truncate sm:max-w-[200px] lg:max-w-[250px] xl:max-w-[350px]"> 51 - {row.getValue("description") || "-"} 52 - </span> 53 - </div> 54 - ); 79 + const { count, ok } = row.original?.metrics || {}; 80 + if (!count || !ok) 81 + return <span className="text-muted-foreground">-</span>; 82 + const rounded = Math.round((ok / count) * 10_000) / 100; 83 + return <Number value={rounded} suffix="%" />; 55 84 }, 56 85 }, 57 86 { 58 - accessorKey: "status", 59 - header: "Status", 87 + accessorKey: "avgLatency", 88 + header: () => <HeaderTooltip>AVG</HeaderTooltip>, 60 89 cell: ({ row }) => { 61 - const { active, status } = row.original; 62 - 63 - if (!active) return <Badge variant="secondary">pause</Badge>; 64 - if (status === "error") return <Badge variant="destructive">down</Badge>; 65 - return <Badge variant="outline">up</Badge>; 90 + const latency = row.original.metrics?.avgLatency; 91 + if (latency) return <Number value={latency} suffix="ms" />; 92 + return <span className="text-muted-foreground">-</span>; 66 93 }, 67 94 }, 68 95 { 69 - accessorKey: "periodicity", 70 - header: "Frequency", 96 + accessorKey: "p95Latency", 97 + header: () => <HeaderTooltip>P95</HeaderTooltip>, 71 98 cell: ({ row }) => { 72 - return <span className="font-mono">{row.getValue("periodicity")}</span>; 99 + const latency = row.original.metrics?.p95Latency; 100 + if (latency) return <Number value={latency} suffix="ms" />; 101 + return <span className="text-muted-foreground">-</span>; 73 102 }, 74 103 }, 75 104 { ··· 83 112 }, 84 113 }, 85 114 ]; 115 + 116 + function HeaderTooltip({ children }: { children: string }) { 117 + return ( 118 + <TooltipProvider> 119 + <Tooltip> 120 + <TooltipTrigger className="underline decoration-dotted"> 121 + {children} 122 + </TooltipTrigger> 123 + <TooltipContent>Data from the last 24h</TooltipContent> 124 + </Tooltip> 125 + </TooltipProvider> 126 + ); 127 + } 128 + 129 + function Number({ value, suffix }: { value: number; suffix: string }) { 130 + return ( 131 + <span className="font-mono"> 132 + {new Intl.NumberFormat("us").format(value).toString()}{" "} 133 + <span className="text-muted-foreground text-xs font-normal"> 134 + {suffix} 135 + </span> 136 + </span> 137 + ); 138 + }
+4 -1
apps/web/src/components/data-table/monitor/data-table-row-actions.tsx
··· 5 5 import { useRouter } from "next/navigation"; 6 6 import type { Row } from "@tanstack/react-table"; 7 7 import { MoreHorizontal } from "lucide-react"; 8 + import { z } from "zod"; 8 9 9 10 import { selectMonitorSchema } from "@openstatus/db/src/schema"; 10 11 import { ··· 37 38 export function DataTableRowActions<TData>({ 38 39 row, 39 40 }: DataTableRowActionsProps<TData>) { 40 - const monitor = selectMonitorSchema.parse(row.original); 41 + const { monitor } = z 42 + .object({ monitor: selectMonitorSchema }) 43 + .parse(row.original); 41 44 const router = useRouter(); 42 45 const { toast } = useToastAction(); 43 46 const [alertOpen, setAlertOpen] = React.useState(false);
+18 -7
apps/web/src/components/tracker/tracker.tsx
··· 77 77 const uptime = getTotalUptimeString(data); 78 78 const _data = addBlackListInfo(data); 79 79 80 - console.log({ incidents }); 81 - 82 80 return ( 83 81 <div className="flex flex-col gap-1.5"> 84 82 <div className="flex justify-between text-sm"> ··· 165 163 type BarProps = Monitor & { blacklist?: string } & Pick< 166 164 TrackerProps, 167 165 "reports" | "incidents" 168 - >; 166 + > & { 167 + className?: string; 168 + }; 169 169 170 - const Bar = ({ count, ok, day, blacklist, reports, incidents }: BarProps) => { 170 + export const Bar = ({ 171 + count, 172 + ok, 173 + day, 174 + blacklist, 175 + reports, 176 + incidents, 177 + className, 178 + }: BarProps) => { 171 179 const [open, setOpen] = React.useState(false); 172 180 const status = getStatusByRatio(ok / count); 173 181 const isIncident = incidents && incidents.length > 0; 174 182 175 183 const { label, variant } = isIncident ? incidentStatus : status; 176 184 177 - const className = tracker({ 185 + const rootClassName = tracker({ 178 186 report: reports && reports.length > 0 ? 30 : undefined, 179 187 variant: blacklist ? "blacklist" : variant, 180 188 }); ··· 187 195 onOpenChange={setOpen} 188 196 > 189 197 <HoverCardTrigger onClick={() => setOpen(true)} asChild> 190 - <div className={className} /> 198 + <div className={cn(rootClassName, className)} /> 191 199 </HoverCardTrigger> 192 200 <HoverCardContent side="top" className="w-auto max-w-[16rem] p-2"> 193 201 {blacklist ? ( ··· 196 204 <div> 197 205 <div className="flex gap-2"> 198 206 <div 199 - className={cn(className, "h-auto w-1 flex-none rounded-full")} 207 + className={cn( 208 + rootClassName, 209 + "h-auto w-1 flex-none rounded-full", 210 + )} 200 211 /> 201 212 <div className="grid flex-1 gap-1"> 202 213 <div className="flex justify-between gap-8 text-sm">