-
Notifications
You must be signed in to change notification settings - Fork 611
feat: logs page v2 #2701
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: logs page v2 #2701
Changes from all commits
Commits
Show all changes
52 commits
Select commit
Hold shift + click to select a range
7759006
feat: add basic components
ogzhanolguncu 5993ae5
feat: add log details section
ogzhanolguncu 4344d3a
feat: move log details to separate component
ogzhanolguncu 42e481d
refactor: log details section
ogzhanolguncu 78eec7b
fix: overflow issue
ogzhanolguncu 20ea6ae
feat: add custom time and calendar
ogzhanolguncu 3e18a34
fix: server-client render issues
ogzhanolguncu d2b8b1f
feat: add time to calendar comp
ogzhanolguncu 1cc48a8
feat: add response status filter
ogzhanolguncu d2a517c
feat: finalize search combobox
ogzhanolguncu 9519fe4
feat: refactor search combobox and add search param persistance
ogzhanolguncu 88667d5
feat: add filter persistance to all components
ogzhanolguncu 9258053
feat: add highlight effect when row selected
ogzhanolguncu 0fe8ba3
fix: clickhouse and also main pull I guess
chronark 873e64e
feat: add initial data fetch for logs page
ogzhanolguncu 0ae2d82
re organize files and use real data for charts component
ogzhanolguncu 0da98b5
fix: trpc query
ogzhanolguncu 6d231b5
chore: formatter
ogzhanolguncu b9c6346
feat: add badges to request and status
ogzhanolguncu ee7f73a
feat: add new tooltip for time
ogzhanolguncu 7a2f3fc
feat: change how we show json fields in details modal
ogzhanolguncu ad2d539
Merge branch 'main' of github.com:unkeyed/unkey into logs-page
ogzhanolguncu 3c22672
chore: update lock
ogzhanolguncu 029e4d4
feat: add virtual list to logs
ogzhanolguncu 5b21935
chore: add new local
ogzhanolguncu 1714490
refactor: improvement
ogzhanolguncu 444eb2e
refactor: improve query params type safety
ogzhanolguncu 215d7b8
refactor: move click house to internal package
ogzhanolguncu 8fe44eb
chore: fix fmt issues
ogzhanolguncu 610531b
fix: fmt issues and clickhouse imports
ogzhanolguncu 1eee930
fix: pnmp conflict
ogzhanolguncu 5213279
Merge branch 'main' of github.com:unkeyed/unkey into logs-page
ogzhanolguncu cc79423
[autofix.ci] apply automated fixes
autofix-ci[bot] 02b3251
fix: lock quote issue
ogzhanolguncu 230fb5a
chore: add new lock file
ogzhanolguncu d247461
Merge branch 'logs-page' of https://github.com/ogzhanolguncu/unkey in…
ogzhanolguncu cb2b70e
fix: code rabbit issues
ogzhanolguncu 56a14bf
[autofix.ci] apply automated fixes
autofix-ci[bot] 80391cc
fix: revert some rabbit fixes and add review fixes
ogzhanolguncu 9b0f94d
refactor: improve chart
ogzhanolguncu 3bf4794
chore: remove shiki
ogzhanolguncu 7f94805
Merge branch 'logs-page' of https://github.com/ogzhanolguncu/unkey in…
ogzhanolguncu b457ad7
Merge branch 'main' of github.com:unkeyed/unkey into logs-page
ogzhanolguncu d72e3df
chore: add update lock
ogzhanolguncu 4c6312d
feat: add feature flag for logs page
ogzhanolguncu c7f6e62
fix: feature flag opt in types
ogzhanolguncu bb4f927
chore: remove shiki imports
ogzhanolguncu 6d9b7aa
fix: reload and timestamp zindex issue
ogzhanolguncu 92ed909
feat: add loading state to table
ogzhanolguncu 9d10310
fix: use ref instead of direct dom access
ogzhanolguncu c97487b
fix: PR comments
ogzhanolguncu e3ab28e
Merge branch 'main' into logs-page-v2
ogzhanolguncu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| "use client"; | ||
|
|
||
| import { | ||
| type ChartConfig, | ||
| ChartContainer, | ||
| ChartTooltip, | ||
| ChartTooltipContent, | ||
| } from "@/components/ui/chart"; | ||
| import { format } from "date-fns"; | ||
| import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; | ||
| import { useLogSearchParams } from "../query-state"; | ||
|
|
||
| export type Log = { | ||
| request_id: string; | ||
| time: number; | ||
| workspace_id: string; | ||
| host: string; | ||
| method: string; | ||
| path: string; | ||
| request_headers: string[]; | ||
| request_body: string; | ||
| response_status: number; | ||
| response_headers: string[]; | ||
| response_body: string; | ||
| error: string; | ||
| service_latency: number; | ||
| }; | ||
|
|
||
| const chartConfig = { | ||
| success: { | ||
| label: "Success", | ||
| color: "hsl(var(--chart-3))", | ||
| }, | ||
| warning: { | ||
| label: "Warning", | ||
| color: "hsl(var(--chart-4))", | ||
| }, | ||
| error: { | ||
| label: "Error", | ||
| color: "hsl(var(--chart-1))", | ||
| }, | ||
| } satisfies ChartConfig; | ||
|
|
||
| export function LogsChart({ logs }: { logs: Log[] }) { | ||
| const { searchParams } = useLogSearchParams(); | ||
| const data = aggregateData(logs, searchParams.startTime, searchParams.endTime ?? Date.now()); | ||
|
|
||
| return ( | ||
| <ChartContainer config={chartConfig} className="h-[125px] w-full"> | ||
| <BarChart accessibilityLayer data={data}> | ||
| <XAxis | ||
| dataKey="date" | ||
| tickLine={false} | ||
| axisLine={false} | ||
| dx={-40} | ||
| tickFormatter={(value) => { | ||
| const date = new Date(value); | ||
| return date.toLocaleDateString("en-US", { | ||
| month: "short", | ||
| day: "2-digit", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| hour12: true, | ||
| }); | ||
| }} | ||
| /> | ||
| <CartesianGrid | ||
| strokeDasharray="2" | ||
| stroke={"hsl(var(--cartesian-grid-stroke))"} | ||
| vertical={false} | ||
| /> | ||
| <ChartTooltip | ||
| content={ | ||
| <ChartTooltipContent | ||
| className="w-[200px]" | ||
| nameKey="views" | ||
| labelFormatter={(value) => { | ||
| return new Date(value).toLocaleDateString("en-US", { | ||
| month: "short", | ||
| day: "numeric", | ||
| year: "numeric", | ||
| hour: "2-digit", | ||
| minute: "2-digit", | ||
| hour12: true, | ||
| }); | ||
| }} | ||
| /> | ||
| } | ||
| /> | ||
| <Bar dataKey="success" stackId="a" fill="var(--color-success)" radius={3} /> | ||
| <Bar dataKey="warning" stackId="a" fill="var(--color-warning)" radius={3} /> | ||
| <Bar dataKey="error" stackId="a" fill="var(--color-error)" radius={3} /> | ||
| </BarChart> | ||
| </ChartContainer> | ||
| ); | ||
| } | ||
|
|
||
| function aggregateData(data: Log[], startTime: number, endTime: number) { | ||
| const aggregatedData: { | ||
| date: string; | ||
| success: number; | ||
| warning: number; | ||
| error: number; | ||
| }[] = []; | ||
|
|
||
| const intervalMs = 60 * 1000 * 10; // 10 minutes | ||
|
|
||
| if (data.length === 0) { | ||
| return aggregatedData; | ||
| } | ||
|
|
||
| const buckets = new Map(); | ||
|
|
||
| // Create a bucket for each 10 minute interval | ||
| for (let timestamp = startTime; timestamp < endTime; timestamp += intervalMs) { | ||
| buckets.set(timestamp, { | ||
| date: format(timestamp, "yyyy-MM-dd'T'HH:mm:ss"), | ||
| success: 0, | ||
| warning: 0, | ||
| error: 0, | ||
| }); | ||
| } | ||
|
|
||
| // For each log, find its bucket then increment the appropriate counter | ||
| for (const log of data) { | ||
| const bucketIndex = Math.floor((log.time - startTime) / intervalMs); | ||
| const bucket = buckets.get(startTime + bucketIndex * intervalMs); | ||
|
|
||
| if (bucket) { | ||
| const status = log.response_status; | ||
| if (status >= 200 && status < 300) { | ||
| bucket.success++; | ||
| } else if (status >= 400 && status < 500) { | ||
| bucket.warning++; | ||
| } else if (status >= 500) { | ||
| bucket.error++; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return Array.from(buckets.values()); | ||
| } | ||
161 changes: 161 additions & 0 deletions
161
apps/dashboard/app/(app)/logs/components/filters/components/custom-date-filter.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,161 @@ | ||
| "use client"; | ||
|
|
||
| import { format, setHours, setMinutes, setSeconds } from "date-fns"; | ||
| import type { DateRange } from "react-day-picker"; | ||
|
|
||
| import { Button } from "@/components/ui/button"; | ||
| import { Calendar } from "@/components/ui/calendar"; | ||
| import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; | ||
| import { cn } from "@/lib/utils"; | ||
| import { ArrowRight, Calendar as CalendarIcon } from "lucide-react"; | ||
| import { useEffect, useState } from "react"; | ||
| import { useLogSearchParams } from "../../../query-state"; | ||
| import TimeSplitInput from "./time-split"; | ||
|
|
||
| export function DatePickerWithRange({ className }: React.HTMLAttributes<HTMLDivElement>) { | ||
| const [interimDate, setInterimDate] = useState<DateRange>({ | ||
| from: new Date(), | ||
| to: new Date(), | ||
| }); | ||
| const [finalDate, setFinalDate] = useState<DateRange>(); | ||
| const [startTime, setStartTime] = useState({ HH: "09", mm: "00", ss: "00" }); | ||
| const [endTime, setEndTime] = useState({ HH: "17", mm: "00", ss: "00" }); | ||
| const [open, setOpen] = useState(false); | ||
| const { searchParams, setSearchParams } = useLogSearchParams(); | ||
|
|
||
| useEffect(() => { | ||
| if (searchParams.startTime && searchParams.endTime) { | ||
| const from = new Date(searchParams.startTime); | ||
| const to = new Date(searchParams.endTime); | ||
| setFinalDate({ from, to }); | ||
| setInterimDate({ from, to }); | ||
| setStartTime({ | ||
| HH: from.getHours().toString().padStart(2, "0"), | ||
| mm: from.getMinutes().toString().padStart(2, "0"), | ||
| ss: from.getSeconds().toString().padStart(2, "0"), | ||
| }); | ||
| setEndTime({ | ||
| HH: to.getHours().toString().padStart(2, "0"), | ||
| mm: to.getMinutes().toString().padStart(2, "0"), | ||
| ss: to.getSeconds().toString().padStart(2, "0"), | ||
| }); | ||
| } | ||
| }, [searchParams.startTime, searchParams.endTime]); | ||
ogzhanolguncu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const handleFinalDate = (interimDate: DateRange | undefined) => { | ||
| setOpen(false); | ||
|
|
||
| if (interimDate?.from) { | ||
| let mergedFrom = setHours(interimDate.from, Number(startTime.HH)); | ||
| mergedFrom = setMinutes(mergedFrom, Number(startTime.mm)); | ||
| mergedFrom = setSeconds(mergedFrom, Number(startTime.ss)); | ||
|
|
||
| let mergedTo: Date; | ||
| if (interimDate.to) { | ||
| mergedTo = setHours(interimDate.to, Number(endTime.HH)); | ||
| mergedTo = setMinutes(mergedTo, Number(endTime.mm)); | ||
| mergedTo = setSeconds(mergedTo, Number(endTime.ss)); | ||
| } else { | ||
| mergedTo = setHours(interimDate.from, Number(endTime.HH)); | ||
| mergedTo = setMinutes(mergedTo, Number(endTime.mm)); | ||
| mergedTo = setSeconds(mergedTo, Number(endTime.ss)); | ||
| } | ||
|
|
||
| setFinalDate({ from: mergedFrom, to: mergedTo }); | ||
| setSearchParams({ | ||
| startTime: mergedFrom.getTime(), | ||
| endTime: mergedTo.getTime(), | ||
| }); | ||
| } else { | ||
| setFinalDate(interimDate); | ||
| setSearchParams({ | ||
| startTime: undefined, | ||
| endTime: undefined, | ||
| }); | ||
| } | ||
| }; | ||
ogzhanolguncu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| return ( | ||
| <div className={cn("grid gap-2", className)}> | ||
| <Popover open={open} onOpenChange={setOpen}> | ||
| <PopoverTrigger asChild> | ||
| <div | ||
| id="date" | ||
| className={cn( | ||
| "justify-start text-left font-normal flex gap-2 items-center", | ||
| !finalDate && "text-muted-foreground", | ||
| )} | ||
| > | ||
| <div className="flex gap-2 items-center w-fit"> | ||
| <div> | ||
| <CalendarIcon className="h-4 w-4" /> | ||
| </div> | ||
| {finalDate?.from ? ( | ||
| finalDate.to ? ( | ||
| <div className="truncate"> | ||
| {format(finalDate.from, "LLL dd, y")} - {format(finalDate.to, "LLL dd, y")} | ||
| </div> | ||
| ) : ( | ||
| format(finalDate.from, "LLL dd, y") | ||
| ) | ||
| ) : ( | ||
| <span>Custom</span> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </PopoverTrigger> | ||
| <PopoverContent className="w-auto p-0 bg-background"> | ||
| <Calendar | ||
| initialFocus | ||
| mode="range" | ||
| defaultMonth={interimDate?.from} | ||
| selected={interimDate} | ||
| onSelect={(date) => | ||
| setInterimDate({ | ||
| from: date?.from, | ||
| to: date?.to, | ||
| }) | ||
| } | ||
| /> | ||
| <div className="flex flex-col gap-2"> | ||
| <div className="border-t border-border" /> | ||
| <div className="flex gap-2 items-center w-full justify-evenly"> | ||
| <TimeSplitInput | ||
| type="start" | ||
| startTime={startTime} | ||
| endTime={endTime} | ||
| time={startTime} | ||
| setTime={setStartTime} | ||
| setStartTime={setStartTime} | ||
| setEndTime={setEndTime} | ||
| startDate={interimDate.from ?? new Date()} | ||
| endDate={interimDate.to ?? new Date()} | ||
| /> | ||
ogzhanolguncu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <ArrowRight strokeWidth={1.5} size={14} /> | ||
| <TimeSplitInput | ||
| type="end" | ||
| startTime={startTime} | ||
| endTime={endTime} | ||
| time={endTime} | ||
| setTime={setEndTime} | ||
| setStartTime={setStartTime} | ||
| setEndTime={setEndTime} | ||
| startDate={interimDate.from ?? new Date()} | ||
| endDate={interimDate.to ?? new Date()} | ||
| /> | ||
| </div> | ||
| <div className="border-t border-border" /> | ||
| </div> | ||
| <div className="flex gap-2 p-2 w-full justify-end bg-background-subtle"> | ||
| <Button size="sm" variant="outline" onClick={() => handleFinalDate(undefined)}> | ||
| Clear | ||
| </Button> | ||
| <Button size="sm" variant="primary" onClick={() => handleFinalDate(interimDate)}> | ||
| Apply | ||
| </Button> | ||
| </div> | ||
| </PopoverContent> | ||
| </Popover> | ||
| </div> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.