Openstatus www.openstatus.dev

feat: sort regions table (#976)

* feat: sort monitor regions table

* fix: cell class name

* ci: apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

authored by

Maximilian Kaske
autofix-ci[bot]
and committed by
GitHub
6f3ef73a 1a8bf3fd

+281 -14
+96
apps/web/src/components/data-table/single-region/columns.tsx
··· 1 + "use client"; 2 + 3 + import { SimpleChart } from "@/components/monitor-charts/simple-chart"; 4 + import { formatNumber } from "@/components/monitor-dashboard/metrics-card"; 5 + import type { Region, ResponseTimeMetricsByRegion } from "@openstatus/tinybird"; 6 + import { flyRegionsDict } from "@openstatus/utils"; 7 + import type { ColumnDef } from "@tanstack/react-table"; 8 + import { DataTableColumnHeader } from "./data-table-column-header"; 9 + 10 + export interface RegionWithMetrics { 11 + data: (Partial<Record<Region, string>> & { timestamp: string })[]; 12 + metrics?: ResponseTimeMetricsByRegion; 13 + region: Region; 14 + } 15 + 16 + export const columns: ColumnDef<RegionWithMetrics>[] = [ 17 + { 18 + accessorKey: "region", 19 + header: "Region", 20 + cell: ({ row }) => { 21 + const region = row.getValue("region") as Region; 22 + const { code, flag, location } = flyRegionsDict[region]; 23 + return ( 24 + <div> 25 + <p className="text-muted-foreground text-xs">{location}</p> 26 + <p className="font-mono text-xs"> 27 + {flag} {code} 28 + </p> 29 + </div> 30 + ); 31 + }, 32 + meta: { 33 + headerClassName: "w-[100px]", 34 + }, 35 + }, 36 + { 37 + accessorKey: "data", 38 + header: "Trend", 39 + cell: ({ row }) => { 40 + const data = row.getValue("data") as RegionWithMetrics["data"]; 41 + const region = row.getValue("region") as Region; 42 + return <SimpleChart data={data} region={region} />; 43 + }, 44 + meta: { 45 + headerClassName: "min-w-[300px] w-full", 46 + }, 47 + }, 48 + { 49 + accessorKey: "p50", 50 + header: ({ column }) => ( 51 + <DataTableColumnHeader column={column} title="P50" /> 52 + ), 53 + accessorFn: (row) => row.metrics?.p50Latency, 54 + cell: ({ row }) => { 55 + const p50 = row.getValue("p50") as number; 56 + return ( 57 + <div className="whitespace-nowrap"> 58 + <span className="font-mono">{formatNumber(p50)}</span>{" "} 59 + <span className="font-normal text-muted-foreground text-xs">ms</span> 60 + </div> 61 + ); 62 + }, 63 + }, 64 + { 65 + accessorKey: "p95", 66 + accessorFn: (row) => row.metrics?.p95Latency, 67 + header: ({ column }) => ( 68 + <DataTableColumnHeader column={column} title="P95" /> 69 + ), 70 + cell: ({ row }) => { 71 + const p95 = row.getValue("p95") as number; 72 + return ( 73 + <div className="whitespace-nowrap"> 74 + <span className="font-mono">{formatNumber(p95)}</span>{" "} 75 + <span className="font-normal text-muted-foreground text-xs">ms</span> 76 + </div> 77 + ); 78 + }, 79 + }, 80 + { 81 + accessorKey: "p99", 82 + header: ({ column }) => ( 83 + <DataTableColumnHeader column={column} title="P99" /> 84 + ), 85 + accessorFn: (row) => row.metrics?.p99Latency, 86 + cell: ({ row }) => { 87 + const p99 = row.getValue("p99") as number; 88 + return ( 89 + <div className="whitespace-nowrap"> 90 + <span className="font-mono">{formatNumber(p99)}</span>{" "} 91 + <span className="font-normal text-muted-foreground text-xs">ms</span> 92 + </div> 93 + ); 94 + }, 95 + }, 96 + ];
+66
apps/web/src/components/data-table/single-region/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 + }
+92
apps/web/src/components/data-table/single-region/data-table.tsx
··· 1 + "use client"; 2 + 3 + import type { ColumnDef, SortingState } from "@tanstack/react-table"; 4 + import { 5 + flexRender, 6 + getCoreRowModel, 7 + getSortedRowModel, 8 + useReactTable, 9 + } from "@tanstack/react-table"; 10 + import * as React from "react"; 11 + 12 + import { 13 + Table, 14 + TableBody, 15 + TableCell, 16 + TableHead, 17 + TableHeader, 18 + TableRow, 19 + } from "@openstatus/ui"; 20 + 21 + interface DataTableProps<TData, TValue> { 22 + columns: ColumnDef<TData, TValue>[]; 23 + data: TData[]; 24 + } 25 + 26 + export function DataTable<TData, TValue>({ 27 + columns, 28 + data, 29 + }: DataTableProps<TData, TValue>) { 30 + const [sorting, setSorting] = React.useState<SortingState>([]); 31 + const table = useReactTable({ 32 + data, 33 + columns, 34 + state: { sorting }, 35 + getCoreRowModel: getCoreRowModel(), 36 + onSortingChange: setSorting, 37 + getSortedRowModel: getSortedRowModel(), 38 + }); 39 + 40 + return ( 41 + <div className="rounded-md border"> 42 + <Table> 43 + <TableHeader className="bg-muted/50"> 44 + {table.getHeaderGroups().map((headerGroup) => ( 45 + <TableRow key={headerGroup.id} className="hover:bg-transparent"> 46 + {headerGroup.headers.map((header) => { 47 + return ( 48 + <TableHead 49 + key={header.id} 50 + className={header.column.columnDef.meta?.headerClassName} 51 + > 52 + {header.isPlaceholder 53 + ? null 54 + : flexRender( 55 + header.column.columnDef.header, 56 + header.getContext(), 57 + )} 58 + </TableHead> 59 + ); 60 + })} 61 + </TableRow> 62 + ))} 63 + </TableHeader> 64 + <TableBody> 65 + {table.getRowModel().rows?.length ? ( 66 + table.getRowModel().rows.map((row) => ( 67 + <TableRow 68 + key={row.id} 69 + data-state={row.getIsSelected() && "selected"} 70 + > 71 + {row.getVisibleCells().map((cell) => ( 72 + <TableCell 73 + key={cell.id} 74 + className={cell.column.columnDef.meta?.cellClassName} 75 + > 76 + {flexRender(cell.column.columnDef.cell, cell.getContext())} 77 + </TableCell> 78 + ))} 79 + </TableRow> 80 + )) 81 + ) : ( 82 + <TableRow> 83 + <TableCell colSpan={columns.length} className="h-24 text-center"> 84 + No results. 85 + </TableCell> 86 + </TableRow> 87 + )} 88 + </TableBody> 89 + </Table> 90 + </div> 91 + ); 92 + }
+16 -7
apps/web/src/components/monitor-charts/combined-chart-wrapper.tsx
··· 11 11 } from "@openstatus/tinybird"; 12 12 import { Toggle } from "@openstatus/ui"; 13 13 14 + import { columns } from "@/components/data-table/single-region/columns"; 15 + import { DataTable } from "@/components/data-table/single-region/data-table"; 14 16 import { IntervalPreset } from "@/components/monitor-dashboard/interval-preset"; 15 17 import { QuantilePreset } from "@/components/monitor-dashboard/quantile-preset"; 16 18 import { RegionsPreset } from "@/components/monitor-dashboard/region-preset"; ··· 18 20 import { usePreferredSettings } from "@/lib/preferred-settings/client"; 19 21 import type { PreferredSettings } from "@/lib/preferred-settings/server"; 20 22 import { Chart } from "./chart"; 21 - import { RegionTable } from "./region-table"; 22 23 import { groupDataByTimestamp } from "./utils"; 23 24 24 25 export function CombinedChartWrapper({ ··· 53 54 54 55 const combinedRegions = preferredSettings?.combinedRegions ?? false; 55 56 57 + const tableData = useMemo( 58 + () => 59 + regions 60 + .map((region) => ({ 61 + region, 62 + data: chartData.data, 63 + metrics: metricsByRegion.find((metrics) => metrics.region === region), 64 + })) 65 + .filter((row) => !!row.metrics), 66 + [regions, chartData, metricsByRegion], 67 + ); 68 + 56 69 return ( 57 70 <> 58 71 <div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between"> ··· 82 95 <IntervalPreset interval={interval} /> 83 96 </div> 84 97 </div> 85 - <div className="grid gap-3"> 98 + <div> 86 99 {combinedRegions ? ( 87 100 <Chart data={chartData.data} regions={regions} /> 88 101 ) : ( 89 - <RegionTable 90 - metricsByRegion={metricsByRegion} 91 - regions={regions} 92 - data={chartData} 93 - /> 102 + <DataTable columns={columns} data={tableData} /> 94 103 )} 95 104 </div> 96 105 </>
+3 -7
apps/web/src/components/monitor-charts/region-table.tsx
··· 23 23 caption?: string; 24 24 } 25 25 26 + /** 27 + * @deprecated use the /region/data-table.tsx component instead, this is only used for the content blog posts 28 + */ 26 29 export function RegionTable({ 27 30 regions, 28 31 data, 29 32 metricsByRegion, 30 33 caption = "A list of all the selected regions.", 31 34 }: RegionTableProps) { 32 - // console.log(JSON.stringify({ regions, data, metricsByRegion }, null, 2)); 33 35 return ( 34 36 <Table> 35 37 <TableCaption>{caption}</TableCaption> ··· 81 83 ); 82 84 })} 83 85 </TableBody> 84 - {/* <TableFooter> 85 - <TableRow> 86 - <TableCell colSpan={4}>Total</TableCell> 87 - <TableCell className="text-right">0</TableCell> 88 - </TableRow> 89 - </TableFooter> */} 90 86 </Table> 91 87 ); 92 88 }
+8
apps/web/src/react-table.d.ts
··· 1 + import "@tanstack/react-table"; 2 + 3 + declare module "@tanstack/react-table" { 4 + interface ColumnMeta { 5 + headerClassName?: string; 6 + cellClassName?: string; 7 + } 8 + }