-
Notifications
You must be signed in to change notification settings - Fork 521
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
feat: New logs page #2143
feat: New logs page #2143
Changes from all commits
7759006
5993ae5
4344d3a
42e481d
78eec7b
20ea6ae
3e18a34
d2b8b1f
1cc48a8
d2a517c
9519fe4
88667d5
9258053
0fe8ba3
873e64e
0ae2d82
0da98b5
6d231b5
b9c6346
ee7f73a
7a2f3fc
ad2d539
3c22672
029e4d4
5b21935
1714490
444eb2e
215d7b8
8fe44eb
610531b
1eee930
5213279
cc79423
02b3251
230fb5a
d247461
cb2b70e
56a14bf
80391cc
9b0f94d
3bf4794
7f94805
b457ad7
542e577
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
"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 | ||
); | ||
|
||
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, | ||
}); | ||
} | ||
console.log(buckets); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove console.log statement Remove debugging console.log statement before deploying to production. - console.log(buckets); |
||
|
||
// 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()); | ||
} |
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" }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where do these defaults come from? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's just a random starting hour. Do you have something else on your mind? |
||
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, | ||
}); | ||
} | ||
}; | ||
|
||
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, | ||
}) | ||
} | ||
/> | ||
ogzhanolguncu marked this conversation as resolved.
Show resolved
Hide resolved
|
||
<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()} | ||
/> | ||
<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> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix inconsistency in color variable usage
The Bar components use different color variables (
--color-success
) than those defined in chartConfig (--chart-3
). This could lead to inconsistent styling.📝 Committable suggestion