tangled
alpha
login
or
join now
openstatus.dev
/
openstatus
5
fork
atom
Openstatus
www.openstatus.dev
5
fork
atom
overview
issues
pulls
pipelines
chore: add quantile, visit shortcut, add sorting
Maximilian Kaske
2 years ago
92ecfd4e
d2dfc9b2
+210
-42
7 changed files
expand all
collapse all
unified
split
apps
web
src
components
data-table
data-table-column-header.tsx
data-table.tsx
monitor
columns.tsx
data-table-column-header.tsx
data-table-toolbar.tsx
data-table.tsx
status-page
columns.tsx
+5
-5
apps/web/src/components/data-table/data-table-column-header.tsx
···
1
1
import type { Column } from "@tanstack/react-table";
2
2
-
import { ChevronsUpDown, SortAsc, SortDesc } from "lucide-react";
2
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
40
-
<SortDesc className="ml-2 h-4 w-4" />
40
40
+
<ArrowUp className="ml-2 h-4 w-4" />
41
41
) : column.getIsSorted() === "asc" ? (
42
42
-
<SortAsc className="ml-2 h-4 w-4" />
42
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
50
-
<SortAsc className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
50
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
54
-
<SortDesc className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
54
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
87
-
<TableHeader>
87
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
97
-
header.getContext(),
97
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
124
-
cell.getContext(),
124
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
34
+
import { DataTableColumnHeader } from "./data-table-column-header";
35
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
92
+
},
93
93
+
filterFn: (row, _id, value) => {
94
94
+
if (!Array.isArray(value)) return true;
95
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
95
-
header: "Name",
101
101
+
header: ({ column }) => (
102
102
+
<DataTableColumnHeader column={column} title="Name" />
103
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
156
-
<HeaderTooltip label="Last 7 days" content="UTC time period" />
164
164
+
<HeaderTooltip text="UTC time period">
165
165
+
<span className="underline decoration-dotted">Last 7 days</span>
166
166
+
</HeaderTooltip>
157
167
),
158
168
cell: ({ row }) => {
159
169
const tracker = new Tracker({
···
190
200
},
191
201
{
192
202
accessorKey: "uptime",
193
193
-
header: () => (
194
194
-
<HeaderTooltip label="Uptime" content="Data from the last 24h" />
203
203
+
header: ({ column }) => (
204
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
212
+
},
213
213
+
sortingFn: (rowA, rowB, columnId) => {
214
214
+
const valueA = rowA.getValue(columnId) as number | undefined;
215
215
+
const valueB = rowB.getValue(columnId) as number | undefined;
216
216
+
if (!valueA || !valueB) return 0;
217
217
+
return valueA - valueB;
202
218
},
203
219
},
204
220
{
205
221
accessorKey: "p50Latency",
206
206
-
header: () => (
207
207
-
<HeaderTooltip label="P50" content="Data from the last 24h" />
222
222
+
accessorFn: (row) => row.metrics?.p50Latency,
223
223
+
header: ({ column }) => (
224
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
231
+
sortingFn: (rowA, rowB, columnId) => {
232
232
+
const valueA = rowA.getValue(columnId) as number | undefined;
233
233
+
const valueB = rowB.getValue(columnId) as number | undefined;
234
234
+
if (!valueA || !valueB) return 0;
235
235
+
return valueA - valueB;
236
236
+
},
237
237
+
},
238
238
+
{
239
239
+
accessorKey: "p75Latency",
240
240
+
accessorFn: (row) => row.metrics?.p75Latency,
241
241
+
header: ({ column }) => (
242
242
+
<DataTableColumnHeader column={column} title="P75" />
243
243
+
),
244
244
+
cell: ({ row }) => {
245
245
+
const latency = row.original.metrics?.p75Latency;
246
246
+
if (latency) return <DisplayNumber value={latency} suffix="ms" />;
247
247
+
return <span className="text-muted-foreground">-</span>;
248
248
+
},
249
249
+
sortingFn: (rowA, rowB, columnId) => {
250
250
+
const valueA = rowA.getValue(columnId) as number | undefined;
251
251
+
const valueB = rowB.getValue(columnId) as number | undefined;
252
252
+
if (!valueA || !valueB) return 0;
253
253
+
return valueA - valueB;
254
254
+
},
214
255
},
215
256
{
216
257
accessorKey: "p95Latency",
217
217
-
header: () => (
218
218
-
<HeaderTooltip label="P95" content="Data from the last 24h" />
258
258
+
accessorFn: (row) => row.metrics?.p95Latency,
259
259
+
header: ({ column }) => (
260
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
267
+
sortingFn: (rowA, rowB, columnId) => {
268
268
+
const valueA = rowA.getValue(columnId) as number | undefined;
269
269
+
const valueB = rowB.getValue(columnId) as number | undefined;
270
270
+
if (!valueA || !valueB) return 0;
271
271
+
return valueA - valueB;
272
272
+
},
273
273
+
},
274
274
+
{
275
275
+
accessorKey: "p99Latency",
276
276
+
accessorFn: (row) => row.metrics?.p99Latency,
277
277
+
header: ({ column }) => (
278
278
+
<DataTableColumnHeader column={column} title="P99" />
279
279
+
),
280
280
+
cell: ({ row }) => {
281
281
+
const latency = row.original.metrics?.p99Latency;
282
282
+
if (latency) return <DisplayNumber value={latency} suffix="ms" />;
283
283
+
return <span className="text-muted-foreground">-</span>;
284
284
+
},
285
285
+
sortingFn: (rowA, rowB, columnId) => {
286
286
+
const valueA = rowA.getValue(columnId) as number | undefined;
287
287
+
const valueB = rowB.getValue(columnId) as number | undefined;
288
288
+
if (!valueA || !valueB) return 0;
289
289
+
return valueA - valueB;
290
290
+
},
225
291
},
226
292
{
227
293
id: "actions",
···
235
301
},
236
302
];
237
303
238
238
-
function HeaderTooltip({ label, content }: { label: string; content: string }) {
304
304
+
function HeaderTooltip({
305
305
+
text,
306
306
+
children,
307
307
+
}: {
308
308
+
text: string;
309
309
+
children: ReactNode;
310
310
+
}) {
239
311
return (
240
312
<TooltipProvider>
241
313
<Tooltip>
242
242
-
<TooltipTrigger className="underline decoration-dotted">
243
243
-
{label}
244
244
-
</TooltipTrigger>
245
245
-
<TooltipContent>{content}</TooltipContent>
314
314
+
<TooltipTrigger suppressHydrationWarning>{children}</TooltipTrigger>
315
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
1
+
import type { Column } from "@tanstack/react-table";
2
2
+
import { ArrowDown, ArrowUp, ChevronsUpDown } from "lucide-react";
3
3
+
4
4
+
import {
5
5
+
Button,
6
6
+
DropdownMenu,
7
7
+
DropdownMenuContent,
8
8
+
DropdownMenuItem,
9
9
+
DropdownMenuTrigger,
10
10
+
} from "@openstatus/ui";
11
11
+
12
12
+
import { cn } from "@/lib/utils";
13
13
+
14
14
+
interface DataTableColumnHeaderProps<TData, TValue>
15
15
+
extends React.HTMLAttributes<HTMLDivElement> {
16
16
+
column: Column<TData, TValue>;
17
17
+
title: string;
18
18
+
}
19
19
+
20
20
+
export function DataTableColumnHeader<TData, TValue>({
21
21
+
column,
22
22
+
title,
23
23
+
className,
24
24
+
}: DataTableColumnHeaderProps<TData, TValue>) {
25
25
+
if (!column.getCanSort()) {
26
26
+
return <div className={cn(className)}>{title}</div>;
27
27
+
}
28
28
+
29
29
+
return (
30
30
+
<div className={cn("flex items-center space-x-2", className)}>
31
31
+
<DropdownMenu>
32
32
+
<DropdownMenuTrigger asChild>
33
33
+
<Button
34
34
+
variant="ghost"
35
35
+
size="sm"
36
36
+
className="-ml-3 h-8 data-[state=open]:bg-accent"
37
37
+
>
38
38
+
<span>{title}</span>
39
39
+
{column.getIsSorted() === "desc" ? (
40
40
+
<ArrowUp className="ml-2 h-4 w-4" />
41
41
+
) : column.getIsSorted() === "asc" ? (
42
42
+
<ArrowDown className="ml-2 h-4 w-4" />
43
43
+
) : (
44
44
+
<ChevronsUpDown className="ml-2 h-4 w-4" />
45
45
+
)}
46
46
+
</Button>
47
47
+
</DropdownMenuTrigger>
48
48
+
<DropdownMenuContent align="start">
49
49
+
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
50
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
54
+
<ArrowUp className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
55
55
+
Desc
56
56
+
</DropdownMenuItem>
57
57
+
{/* <DropdownMenuSeparator />
58
58
+
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
59
59
+
<EyeOff className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
60
60
+
Hide
61
61
+
</DropdownMenuItem> */}
62
62
+
</DropdownMenuContent>
63
63
+
</DropdownMenu>
64
64
+
</div>
65
65
+
);
66
66
+
}
+15
apps/web/src/components/data-table/monitor/data-table-toolbar.tsx
···
50
50
]}
51
51
/>
52
52
)}
53
53
+
{table.getColumn("active") && (
54
54
+
<DataTableFacetedFilter
55
55
+
column={table.getColumn("active")}
56
56
+
title="Active"
57
57
+
options={[
58
58
+
{ label: "True", value: true },
59
59
+
{ label: "False", value: false },
60
60
+
]}
61
61
+
/>
62
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
73
+
</div>
74
74
+
<div className="h-8 self-end rounded-lg border bg-muted/50 px-3">
75
75
+
<p className="text-muted-foreground text-xs">
76
76
+
Quantiles and Uptime are aggregated data from the last 24h.
77
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
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
15
+
getFacetedUniqueValues,
14
16
getFilteredRowModel,
15
17
getPaginationRowModel,
18
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
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
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
81
+
onSortingChange: setSorting,
82
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
81
-
const map = new Map();
88
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
97
-
}
98
98
-
if (columnId === "public") {
99
99
-
const values = table
100
100
-
.getCoreRowModel()
101
101
-
.flatRows.map((row) => row.getValue(columnId)) as boolean[];
102
102
-
const publicValue = values.filter((v) => v === true).length;
103
103
-
map.set(true, publicValue);
104
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
17
-
import { Check } from "lucide-react";
17
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
31
-
<Link
32
32
-
href={`./status-pages/${row.original.id}/edit`}
33
33
-
className="group flex items-center gap-2"
34
34
-
>
35
35
-
<span className="max-w-[125px] truncate group-hover:underline">
36
36
-
{row.getValue("title")}
37
37
-
</span>
38
38
-
{row.original.maintenancesToPages.length > 0 ? (
39
39
-
<Badge>Maintenance</Badge>
40
40
-
) : null}
41
41
-
</Link>
31
31
+
<div className="flex items-center gap-1">
32
32
+
<Link
33
33
+
href={`./status-pages/${row.original.id}/edit`}
34
34
+
className="group flex items-center gap-2"
35
35
+
>
36
36
+
<span className="max-w-[125px] truncate group-hover:underline">
37
37
+
{row.getValue("title")}
38
38
+
</span>
39
39
+
</Link>
40
40
+
<TooltipProvider>
41
41
+
<Tooltip delayDuration={100}>
42
42
+
<TooltipTrigger>
43
43
+
<a
44
44
+
href={
45
45
+
process.env.NODE_ENV === "production"
46
46
+
? `https://${row.original.slug}.openstatus.dev`
47
47
+
: `/status-page/${row.original.slug}`
48
48
+
}
49
49
+
target="_blank"
50
50
+
rel="noreferrer"
51
51
+
className="text-muted-foreground hover:text-foreground"
52
52
+
>
53
53
+
<ArrowUpRight className="h-4 w-4 flex-shrink-0" />
54
54
+
</a>
55
55
+
</TooltipTrigger>
56
56
+
<TooltipContent>Visit page</TooltipContent>
57
57
+
</Tooltip>
58
58
+
</TooltipProvider>
59
59
+
</div>
42
60
);
43
61
},
44
62
},