Skip to content

Commit b590ef4

Browse files
Merge branch 'main' into new-contri
2 parents c8d33aa + 0e42754 commit b590ef4

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+3258
-166
lines changed

apps/dashboard/app/(app)/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default async function Layout({ children, breadcrumb }: LayoutProps) {
4343
/>
4444

4545
<div className="isolate bg-background lg:border-l border-t lg:rounded-tl-[0.625rem] border-border w-full overflow-x-auto flex flex-col items-center lg:mt-2">
46-
<div className="w-full max-w-[1152px] p-4 lg:p-8">
46+
<div className="w-full p-4 lg:p-8">
4747
{workspace.enabled ? (
4848
<>
4949
{/* Hacky way to make the breadcrumbs line up with the Teamswitcher on the left, because that also has h12 */}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"use client";
2+
3+
import {
4+
type ChartConfig,
5+
ChartContainer,
6+
ChartTooltip,
7+
ChartTooltipContent,
8+
} from "@/components/ui/chart";
9+
import { format } from "date-fns";
10+
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
11+
import { useLogSearchParams } from "../query-state";
12+
13+
export type Log = {
14+
request_id: string;
15+
time: number;
16+
workspace_id: string;
17+
host: string;
18+
method: string;
19+
path: string;
20+
request_headers: string[];
21+
request_body: string;
22+
response_status: number;
23+
response_headers: string[];
24+
response_body: string;
25+
error: string;
26+
service_latency: number;
27+
};
28+
29+
const chartConfig = {
30+
success: {
31+
label: "Success",
32+
color: "hsl(var(--chart-3))",
33+
},
34+
warning: {
35+
label: "Warning",
36+
color: "hsl(var(--chart-4))",
37+
},
38+
error: {
39+
label: "Error",
40+
color: "hsl(var(--chart-1))",
41+
},
42+
} satisfies ChartConfig;
43+
44+
export function LogsChart({ logs }: { logs: Log[] }) {
45+
const { searchParams } = useLogSearchParams();
46+
const data = aggregateData(logs, searchParams.startTime, searchParams.endTime ?? Date.now());
47+
48+
return (
49+
<ChartContainer config={chartConfig} className="h-[125px] w-full">
50+
<BarChart accessibilityLayer data={data}>
51+
<XAxis
52+
dataKey="date"
53+
tickLine={false}
54+
axisLine={false}
55+
dx={-40}
56+
tickFormatter={(value) => {
57+
const date = new Date(value);
58+
return date.toLocaleDateString("en-US", {
59+
month: "short",
60+
day: "2-digit",
61+
hour: "2-digit",
62+
minute: "2-digit",
63+
hour12: true,
64+
});
65+
}}
66+
/>
67+
<CartesianGrid
68+
strokeDasharray="2"
69+
stroke={"hsl(var(--cartesian-grid-stroke))"}
70+
vertical={false}
71+
/>
72+
<ChartTooltip
73+
content={
74+
<ChartTooltipContent
75+
className="w-[200px]"
76+
nameKey="views"
77+
labelFormatter={(value) => {
78+
return new Date(value).toLocaleDateString("en-US", {
79+
month: "short",
80+
day: "numeric",
81+
year: "numeric",
82+
hour: "2-digit",
83+
minute: "2-digit",
84+
hour12: true,
85+
});
86+
}}
87+
/>
88+
}
89+
/>
90+
<Bar dataKey="success" stackId="a" fill="var(--color-success)" radius={3} />
91+
<Bar dataKey="warning" stackId="a" fill="var(--color-warning)" radius={3} />
92+
<Bar dataKey="error" stackId="a" fill="var(--color-error)" radius={3} />
93+
</BarChart>
94+
</ChartContainer>
95+
);
96+
}
97+
98+
function aggregateData(data: Log[], startTime: number, endTime: number) {
99+
const aggregatedData: {
100+
date: string;
101+
success: number;
102+
warning: number;
103+
error: number;
104+
}[] = [];
105+
106+
const intervalMs = 60 * 1000 * 10; // 10 minutes
107+
108+
if (data.length === 0) {
109+
return aggregatedData;
110+
}
111+
112+
const buckets = new Map();
113+
114+
// Create a bucket for each 10 minute interval
115+
for (let timestamp = startTime; timestamp < endTime; timestamp += intervalMs) {
116+
buckets.set(timestamp, {
117+
date: format(timestamp, "yyyy-MM-dd'T'HH:mm:ss"),
118+
success: 0,
119+
warning: 0,
120+
error: 0,
121+
});
122+
}
123+
124+
// For each log, find its bucket then increment the appropriate counter
125+
for (const log of data) {
126+
const bucketIndex = Math.floor((log.time - startTime) / intervalMs);
127+
const bucket = buckets.get(startTime + bucketIndex * intervalMs);
128+
129+
if (bucket) {
130+
const status = log.response_status;
131+
if (status >= 200 && status < 300) {
132+
bucket.success++;
133+
} else if (status >= 400 && status < 500) {
134+
bucket.warning++;
135+
} else if (status >= 500) {
136+
bucket.error++;
137+
}
138+
}
139+
}
140+
141+
return Array.from(buckets.values());
142+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"use client";
2+
3+
import { format, setHours, setMinutes, setSeconds } from "date-fns";
4+
import type { DateRange } from "react-day-picker";
5+
6+
import { Button } from "@/components/ui/button";
7+
import { Calendar } from "@/components/ui/calendar";
8+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
9+
import { cn } from "@/lib/utils";
10+
import { ArrowRight, Calendar as CalendarIcon } from "lucide-react";
11+
import { useEffect, useState } from "react";
12+
import { useLogSearchParams } from "../../../query-state";
13+
import TimeSplitInput from "./time-split";
14+
15+
export function DatePickerWithRange({ className }: React.HTMLAttributes<HTMLDivElement>) {
16+
const [interimDate, setInterimDate] = useState<DateRange>({
17+
from: new Date(),
18+
to: new Date(),
19+
});
20+
const [finalDate, setFinalDate] = useState<DateRange>();
21+
const [startTime, setStartTime] = useState({ HH: "09", mm: "00", ss: "00" });
22+
const [endTime, setEndTime] = useState({ HH: "17", mm: "00", ss: "00" });
23+
const [open, setOpen] = useState(false);
24+
const { searchParams, setSearchParams } = useLogSearchParams();
25+
26+
useEffect(() => {
27+
if (searchParams.startTime && searchParams.endTime) {
28+
const from = new Date(searchParams.startTime);
29+
const to = new Date(searchParams.endTime);
30+
setFinalDate({ from, to });
31+
setInterimDate({ from, to });
32+
setStartTime({
33+
HH: from.getHours().toString().padStart(2, "0"),
34+
mm: from.getMinutes().toString().padStart(2, "0"),
35+
ss: from.getSeconds().toString().padStart(2, "0"),
36+
});
37+
setEndTime({
38+
HH: to.getHours().toString().padStart(2, "0"),
39+
mm: to.getMinutes().toString().padStart(2, "0"),
40+
ss: to.getSeconds().toString().padStart(2, "0"),
41+
});
42+
}
43+
}, [searchParams.startTime, searchParams.endTime]);
44+
45+
const handleFinalDate = (interimDate: DateRange | undefined) => {
46+
setOpen(false);
47+
48+
if (interimDate?.from) {
49+
let mergedFrom = setHours(interimDate.from, Number(startTime.HH));
50+
mergedFrom = setMinutes(mergedFrom, Number(startTime.mm));
51+
mergedFrom = setSeconds(mergedFrom, Number(startTime.ss));
52+
53+
let mergedTo: Date;
54+
if (interimDate.to) {
55+
mergedTo = setHours(interimDate.to, Number(endTime.HH));
56+
mergedTo = setMinutes(mergedTo, Number(endTime.mm));
57+
mergedTo = setSeconds(mergedTo, Number(endTime.ss));
58+
} else {
59+
mergedTo = setHours(interimDate.from, Number(endTime.HH));
60+
mergedTo = setMinutes(mergedTo, Number(endTime.mm));
61+
mergedTo = setSeconds(mergedTo, Number(endTime.ss));
62+
}
63+
64+
setFinalDate({ from: mergedFrom, to: mergedTo });
65+
setSearchParams({
66+
startTime: mergedFrom.getTime(),
67+
endTime: mergedTo.getTime(),
68+
});
69+
} else {
70+
setFinalDate(interimDate);
71+
setSearchParams({
72+
startTime: undefined,
73+
endTime: undefined,
74+
});
75+
}
76+
};
77+
78+
return (
79+
<div className={cn("grid gap-2", className)}>
80+
<Popover open={open} onOpenChange={setOpen}>
81+
<PopoverTrigger asChild>
82+
<div
83+
id="date"
84+
className={cn(
85+
"justify-start text-left font-normal flex gap-2 items-center",
86+
!finalDate && "text-muted-foreground",
87+
)}
88+
>
89+
<div className="flex gap-2 items-center w-fit">
90+
<div>
91+
<CalendarIcon className="h-4 w-4" />
92+
</div>
93+
{finalDate?.from ? (
94+
finalDate.to ? (
95+
<div className="truncate">
96+
{format(finalDate.from, "LLL dd, y")} - {format(finalDate.to, "LLL dd, y")}
97+
</div>
98+
) : (
99+
format(finalDate.from, "LLL dd, y")
100+
)
101+
) : (
102+
<span>Custom</span>
103+
)}
104+
</div>
105+
</div>
106+
</PopoverTrigger>
107+
<PopoverContent className="w-auto p-0 bg-background">
108+
<Calendar
109+
initialFocus
110+
mode="range"
111+
defaultMonth={interimDate?.from}
112+
selected={interimDate}
113+
onSelect={(date) =>
114+
setInterimDate({
115+
from: date?.from,
116+
to: date?.to,
117+
})
118+
}
119+
/>
120+
<div className="flex flex-col gap-2">
121+
<div className="border-t border-border" />
122+
<div className="flex gap-2 items-center w-full justify-evenly">
123+
<TimeSplitInput
124+
type="start"
125+
startTime={startTime}
126+
endTime={endTime}
127+
time={startTime}
128+
setTime={setStartTime}
129+
setStartTime={setStartTime}
130+
setEndTime={setEndTime}
131+
startDate={interimDate.from ?? new Date()}
132+
endDate={interimDate.to ?? new Date()}
133+
/>
134+
<ArrowRight strokeWidth={1.5} size={14} />
135+
<TimeSplitInput
136+
type="end"
137+
startTime={startTime}
138+
endTime={endTime}
139+
time={endTime}
140+
setTime={setEndTime}
141+
setStartTime={setStartTime}
142+
setEndTime={setEndTime}
143+
startDate={interimDate.from ?? new Date()}
144+
endDate={interimDate.to ?? new Date()}
145+
/>
146+
</div>
147+
<div className="border-t border-border" />
148+
</div>
149+
<div className="flex gap-2 p-2 w-full justify-end bg-background-subtle">
150+
<Button size="sm" variant="outline" onClick={() => handleFinalDate(undefined)}>
151+
Clear
152+
</Button>
153+
<Button size="sm" variant="primary" onClick={() => handleFinalDate(interimDate)}>
154+
Apply
155+
</Button>
156+
</div>
157+
</PopoverContent>
158+
</Popover>
159+
</div>
160+
);
161+
}

0 commit comments

Comments
 (0)