-
Notifications
You must be signed in to change notification settings - Fork 523
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
45 changed files
with
3,258 additions
and
166 deletions.
There are no files selected for viewing
This file contains 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 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 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]); | ||
|
||
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, | ||
}) | ||
} | ||
/> | ||
<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> | ||
); | ||
} |
Oops, something went wrong.