Skip to content

Commit

Permalink
Merge branch 'main' into new-contri
Browse files Browse the repository at this point in the history
  • Loading branch information
ogzhanolguncu authored Dec 5, 2024
2 parents c8d33aa + 0e42754 commit b590ef4
Show file tree
Hide file tree
Showing 45 changed files with 3,258 additions and 166 deletions.
2 changes: 1 addition & 1 deletion apps/dashboard/app/(app)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default async function Layout({ children, breadcrumb }: LayoutProps) {
/>

<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">
<div className="w-full max-w-[1152px] p-4 lg:p-8">
<div className="w-full p-4 lg:p-8">
{workspace.enabled ? (
<>
{/* Hacky way to make the breadcrumbs line up with the Teamswitcher on the left, because that also has h12 */}
Expand Down
142 changes: 142 additions & 0 deletions apps/dashboard/app/(app)/logs/components/chart.tsx
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());
}
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>
);
}
Loading

0 comments on commit b590ef4

Please sign in to comment.