Openstatus www.openstatus.dev

chore: add quantile, visit shortcut, add sorting

+210 -42
+5 -5
apps/web/src/components/data-table/data-table-column-header.tsx
··· 1 1 import type { Column } from "@tanstack/react-table"; 2 - import { ChevronsUpDown, SortAsc, SortDesc } from "lucide-react"; 2 + import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react"; 3 3 4 4 import { 5 5 Button, ··· 37 37 > 38 38 <span>{title}</span> 39 39 {column.getIsSorted() === "desc" ? ( 40 - <SortDesc className="ml-2 h-4 w-4" /> 40 + <ArrowUp className="ml-2 h-4 w-4" /> 41 41 ) : column.getIsSorted() === "asc" ? ( 42 - <SortAsc className="ml-2 h-4 w-4" /> 42 + <ArrowDown className="ml-2 h-4 w-4" /> 43 43 ) : ( 44 44 <ChevronsUpDown className="ml-2 h-4 w-4" /> 45 45 )} ··· 47 47 </DropdownMenuTrigger> 48 48 <DropdownMenuContent align="start"> 49 49 <DropdownMenuItem onClick={() => column.toggleSorting(false)}> 50 - <SortAsc className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> 50 + <ArrowDown className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> 51 51 Asc 52 52 </DropdownMenuItem> 53 53 <DropdownMenuItem onClick={() => column.toggleSorting(true)}> 54 - <SortDesc className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> 54 + <ArrowUp className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> 55 55 Desc 56 56 </DropdownMenuItem> 57 57 {/* <DropdownMenuSeparator />
+3 -3
apps/web/src/components/data-table/data-table.tsx
··· 84 84 <DataTableToolbar table={table} /> 85 85 <div className="rounded-md border"> 86 86 <Table> 87 - <TableHeader> 87 + <TableHeader className="bg-muted/50"> 88 88 {table.getHeaderGroups().map((headerGroup) => ( 89 89 <TableRow key={headerGroup.id}> 90 90 {headerGroup.headers.map((header) => { ··· 94 94 ? null 95 95 : flexRender( 96 96 header.column.columnDef.header, 97 - header.getContext(), 97 + header.getContext() 98 98 )} 99 99 </TableHead> 100 100 ); ··· 121 121 <TableCell key={cell.id}> 122 122 {flexRender( 123 123 cell.column.columnDef.cell, 124 - cell.getContext(), 124 + cell.getContext() 125 125 )} 126 126 </TableCell> 127 127 ))}
+83 -13
apps/web/src/components/data-table/monitor/columns.tsx
··· 31 31 32 32 import { Eye, EyeOff, Radio, View } from "lucide-react"; 33 33 import { DataTableRowActions } from "./data-table-row-actions"; 34 + import { DataTableColumnHeader } from "./data-table-column-header"; 35 + import type { ReactNode } from "react"; 34 36 35 37 export const columns: ColumnDef<{ 36 38 monitor: Monitor; ··· 87 89 /> 88 90 </div> 89 91 ); 92 + }, 93 + filterFn: (row, _id, value) => { 94 + if (!Array.isArray(value)) return true; 95 + return value.includes(row.original.monitor.active); 90 96 }, 91 97 }, 92 98 { 93 99 accessorKey: "name", 94 100 accessorFn: (row) => row.monitor.name, // used for filtering as name is nested within the monitor object 95 - header: "Name", 101 + header: ({ column }) => ( 102 + <DataTableColumnHeader column={column} title="Name" /> 103 + ), 96 104 cell: ({ row }) => { 97 105 const { name, public: _public } = row.original.monitor; 98 106 return ( ··· 153 161 { 154 162 accessorKey: "tracker", 155 163 header: () => ( 156 - <HeaderTooltip label="Last 7 days" content="UTC time period" /> 164 + <HeaderTooltip text="UTC time period"> 165 + <span className="underline decoration-dotted">Last 7 days</span> 166 + </HeaderTooltip> 157 167 ), 158 168 cell: ({ row }) => { 159 169 const tracker = new Tracker({ ··· 190 200 }, 191 201 { 192 202 accessorKey: "uptime", 193 - header: () => ( 194 - <HeaderTooltip label="Uptime" content="Data from the last 24h" /> 203 + header: ({ column }) => ( 204 + <DataTableColumnHeader column={column} title="Uptime" /> 195 205 ), 196 206 cell: ({ row }) => { 197 207 const { count, ok } = row.original?.metrics || {}; ··· 199 209 return <span className="text-muted-foreground">-</span>; 200 210 const rounded = Math.round((ok / count) * 10_000) / 100; 201 211 return <DisplayNumber value={rounded} suffix="%" />; 212 + }, 213 + sortingFn: (rowA, rowB, columnId) => { 214 + const valueA = rowA.getValue(columnId) as number | undefined; 215 + const valueB = rowB.getValue(columnId) as number | undefined; 216 + if (!valueA || !valueB) return 0; 217 + return valueA - valueB; 202 218 }, 203 219 }, 204 220 { 205 221 accessorKey: "p50Latency", 206 - header: () => ( 207 - <HeaderTooltip label="P50" content="Data from the last 24h" /> 222 + accessorFn: (row) => row.metrics?.p50Latency, 223 + header: ({ column }) => ( 224 + <DataTableColumnHeader column={column} title="P50" /> 208 225 ), 209 226 cell: ({ row }) => { 210 227 const latency = row.original.metrics?.p50Latency; 211 228 if (latency) return <DisplayNumber value={latency} suffix="ms" />; 212 229 return <span className="text-muted-foreground">-</span>; 213 230 }, 231 + sortingFn: (rowA, rowB, columnId) => { 232 + const valueA = rowA.getValue(columnId) as number | undefined; 233 + const valueB = rowB.getValue(columnId) as number | undefined; 234 + if (!valueA || !valueB) return 0; 235 + return valueA - valueB; 236 + }, 237 + }, 238 + { 239 + accessorKey: "p75Latency", 240 + accessorFn: (row) => row.metrics?.p75Latency, 241 + header: ({ column }) => ( 242 + <DataTableColumnHeader column={column} title="P75" /> 243 + ), 244 + cell: ({ row }) => { 245 + const latency = row.original.metrics?.p75Latency; 246 + if (latency) return <DisplayNumber value={latency} suffix="ms" />; 247 + return <span className="text-muted-foreground">-</span>; 248 + }, 249 + sortingFn: (rowA, rowB, columnId) => { 250 + const valueA = rowA.getValue(columnId) as number | undefined; 251 + const valueB = rowB.getValue(columnId) as number | undefined; 252 + if (!valueA || !valueB) return 0; 253 + return valueA - valueB; 254 + }, 214 255 }, 215 256 { 216 257 accessorKey: "p95Latency", 217 - header: () => ( 218 - <HeaderTooltip label="P95" content="Data from the last 24h" /> 258 + accessorFn: (row) => row.metrics?.p95Latency, 259 + header: ({ column }) => ( 260 + <DataTableColumnHeader column={column} title="P95" /> 219 261 ), 220 262 cell: ({ row }) => { 221 263 const latency = row.original.metrics?.p95Latency; 222 264 if (latency) return <DisplayNumber value={latency} suffix="ms" />; 223 265 return <span className="text-muted-foreground">-</span>; 224 266 }, 267 + sortingFn: (rowA, rowB, columnId) => { 268 + const valueA = rowA.getValue(columnId) as number | undefined; 269 + const valueB = rowB.getValue(columnId) as number | undefined; 270 + if (!valueA || !valueB) return 0; 271 + return valueA - valueB; 272 + }, 273 + }, 274 + { 275 + accessorKey: "p99Latency", 276 + accessorFn: (row) => row.metrics?.p99Latency, 277 + header: ({ column }) => ( 278 + <DataTableColumnHeader column={column} title="P99" /> 279 + ), 280 + cell: ({ row }) => { 281 + const latency = row.original.metrics?.p99Latency; 282 + if (latency) return <DisplayNumber value={latency} suffix="ms" />; 283 + return <span className="text-muted-foreground">-</span>; 284 + }, 285 + sortingFn: (rowA, rowB, columnId) => { 286 + const valueA = rowA.getValue(columnId) as number | undefined; 287 + const valueB = rowB.getValue(columnId) as number | undefined; 288 + if (!valueA || !valueB) return 0; 289 + return valueA - valueB; 290 + }, 225 291 }, 226 292 { 227 293 id: "actions", ··· 235 301 }, 236 302 ]; 237 303 238 - function HeaderTooltip({ label, content }: { label: string; content: string }) { 304 + function HeaderTooltip({ 305 + text, 306 + children, 307 + }: { 308 + text: string; 309 + children: ReactNode; 310 + }) { 239 311 return ( 240 312 <TooltipProvider> 241 313 <Tooltip> 242 - <TooltipTrigger className="underline decoration-dotted"> 243 - {label} 244 - </TooltipTrigger> 245 - <TooltipContent>{content}</TooltipContent> 314 + <TooltipTrigger suppressHydrationWarning>{children}</TooltipTrigger> 315 + <TooltipContent>{text}</TooltipContent> 246 316 </Tooltip> 247 317 </TooltipProvider> 248 318 );
+66
apps/web/src/components/data-table/monitor/data-table-column-header.tsx
··· 1 + import type { Column } from "@tanstack/react-table"; 2 + import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react"; 3 + 4 + import { 5 + Button, 6 + DropdownMenu, 7 + DropdownMenuContent, 8 + DropdownMenuItem, 9 + DropdownMenuTrigger, 10 + } from "@openstatus/ui"; 11 + 12 + import { cn } from "@/lib/utils"; 13 + 14 + interface DataTableColumnHeaderProps<TData, TValue> 15 + extends React.HTMLAttributes<HTMLDivElement> { 16 + column: Column<TData, TValue>; 17 + title: string; 18 + } 19 + 20 + export function DataTableColumnHeader<TData, TValue>({ 21 + column, 22 + title, 23 + className, 24 + }: DataTableColumnHeaderProps<TData, TValue>) { 25 + if (!column.getCanSort()) { 26 + return <div className={cn(className)}>{title}</div>; 27 + } 28 + 29 + return ( 30 + <div className={cn("flex items-center space-x-2", className)}> 31 + <DropdownMenu> 32 + <DropdownMenuTrigger asChild> 33 + <Button 34 + variant="ghost" 35 + size="sm" 36 + className="-ml-3 h-8 data-[state=open]:bg-accent" 37 + > 38 + <span>{title}</span> 39 + {column.getIsSorted() === "desc" ? ( 40 + <ArrowUp className="ml-2 h-4 w-4" /> 41 + ) : column.getIsSorted() === "asc" ? ( 42 + <ArrowDown className="ml-2 h-4 w-4" /> 43 + ) : ( 44 + <ChevronsUpDown className="ml-2 h-4 w-4" /> 45 + )} 46 + </Button> 47 + </DropdownMenuTrigger> 48 + <DropdownMenuContent align="start"> 49 + <DropdownMenuItem onClick={() => column.toggleSorting(false)}> 50 + <ArrowDown className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> 51 + Asc 52 + </DropdownMenuItem> 53 + <DropdownMenuItem onClick={() => column.toggleSorting(true)}> 54 + <ArrowUp className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" /> 55 + Desc 56 + </DropdownMenuItem> 57 + {/* <DropdownMenuSeparator /> 58 + <DropdownMenuItem onClick={() => column.toggleVisibility(false)}> 59 + <EyeOff className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" /> 60 + Hide 61 + </DropdownMenuItem> */} 62 + </DropdownMenuContent> 63 + </DropdownMenu> 64 + </div> 65 + ); 66 + }
+15
apps/web/src/components/data-table/monitor/data-table-toolbar.tsx
··· 50 50 ]} 51 51 /> 52 52 )} 53 + {table.getColumn("active") && ( 54 + <DataTableFacetedFilter 55 + column={table.getColumn("active")} 56 + title="Active" 57 + options={[ 58 + { label: "True", value: true }, 59 + { label: "False", value: false }, 60 + ]} 61 + /> 62 + )} 53 63 {isFiltered && ( 54 64 <Button 55 65 variant="ghost" ··· 60 70 <X className="ml-2 h-4 w-4" /> 61 71 </Button> 62 72 )} 73 + </div> 74 + <div className="h-8 self-end rounded-lg border bg-muted/50 px-3"> 75 + <p className="text-muted-foreground text-xs"> 76 + Quantiles and Uptime are aggregated data from the last 24h. 77 + </p> 63 78 </div> 64 79 </div> 65 80 );
+8 -9
apps/web/src/components/data-table/monitor/data-table.tsx
··· 4 4 ColumnDef, 5 5 ColumnFiltersState, 6 6 PaginationState, 7 + SortingState, 7 8 Table as TTable, 8 9 VisibilityState, 9 10 } from "@tanstack/react-table"; ··· 11 12 flexRender, 12 13 getCoreRowModel, 13 14 getFacetedRowModel, 15 + getFacetedUniqueValues, 14 16 getFilteredRowModel, 15 17 getPaginationRowModel, 18 + getSortedRowModel, 16 19 useReactTable, 17 20 } from "@tanstack/react-table"; 18 21 import * as React from "react"; ··· 48 51 defaultColumnFilters = [], 49 52 defaultPagination = { pageIndex: 0, pageSize: 10 }, 50 53 }: DataTableProps<TData, TValue>) { 54 + const [sorting, setSorting] = React.useState<SortingState>([]); 51 55 const [columnFilters, setColumnFilters] = 52 56 React.useState<ColumnFiltersState>(defaultColumnFilters); 53 57 const [columnVisibility, setColumnVisibility] = ··· 66 70 columnFilters, 67 71 columnVisibility, 68 72 pagination, 73 + sorting, 69 74 }, 70 75 onPaginationChange: setPagination, 71 76 getPaginationRowModel: getPaginationRowModel(), ··· 73 78 onColumnVisibilityChange: setColumnVisibility, 74 79 getFilteredRowModel: getFilteredRowModel(), 75 80 getCoreRowModel: getCoreRowModel(), 81 + onSortingChange: setSorting, 82 + getSortedRowModel: getSortedRowModel(), 76 83 getFacetedRowModel: getFacetedRowModel(), 77 84 // TODO: check if we can optimize it - because it gets bigger and bigger with every new filter 78 85 // getFacetedUniqueValues: getFacetedUniqueValues(), 79 86 // REMINDER: We cannot use the default getFacetedUniqueValues as it doesnt support Array of Objects 80 87 getFacetedUniqueValues: (_table: TTable<TData>, columnId: string) => () => { 81 - const map = new Map(); 88 + const map = getFacetedUniqueValues<TData>()(_table, columnId)(); 82 89 if (columnId === "tags") { 83 90 if (tags) { 84 91 for (const tag of tags) { ··· 94 101 map.set(tag.name, tagsNumber); 95 102 } 96 103 } 97 - } 98 - if (columnId === "public") { 99 - const values = table 100 - .getCoreRowModel() 101 - .flatRows.map((row) => row.getValue(columnId)) as boolean[]; 102 - const publicValue = values.filter((v) => v === true).length; 103 - map.set(true, publicValue); 104 - map.set(false, values.length - publicValue); 105 104 } 106 105 return map; 107 106 },
+30 -12
apps/web/src/components/data-table/status-page/columns.tsx
··· 14 14 TooltipTrigger, 15 15 } from "@openstatus/ui"; 16 16 17 - import { Check } from "lucide-react"; 17 + import { ArrowUpRight, Check } from "lucide-react"; 18 18 import { DataTableRowActions } from "./data-table-row-actions"; 19 19 20 20 export const columns: ColumnDef< ··· 28 28 header: "Title", 29 29 cell: ({ row }) => { 30 30 return ( 31 - <Link 32 - href={`./status-pages/${row.original.id}/edit`} 33 - className="group flex items-center gap-2" 34 - > 35 - <span className="max-w-[125px] truncate group-hover:underline"> 36 - {row.getValue("title")} 37 - </span> 38 - {row.original.maintenancesToPages.length > 0 ? ( 39 - <Badge>Maintenance</Badge> 40 - ) : null} 41 - </Link> 31 + <div className="flex items-center gap-1"> 32 + <Link 33 + href={`./status-pages/${row.original.id}/edit`} 34 + className="group flex items-center gap-2" 35 + > 36 + <span className="max-w-[125px] truncate group-hover:underline"> 37 + {row.getValue("title")} 38 + </span> 39 + </Link> 40 + <TooltipProvider> 41 + <Tooltip delayDuration={100}> 42 + <TooltipTrigger> 43 + <a 44 + href={ 45 + process.env.NODE_ENV === "production" 46 + ? `https://${row.original.slug}.openstatus.dev` 47 + : `/status-page/${row.original.slug}` 48 + } 49 + target="_blank" 50 + rel="noreferrer" 51 + className="text-muted-foreground hover:text-foreground" 52 + > 53 + <ArrowUpRight className="h-4 w-4 flex-shrink-0" /> 54 + </a> 55 + </TooltipTrigger> 56 + <TooltipContent>Visit page</TooltipContent> 57 + </Tooltip> 58 + </TooltipProvider> 59 + </div> 42 60 ); 43 61 }, 44 62 },