-
Notifications
You must be signed in to change notification settings - Fork 529
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'main' of https://github.com/unkeyed/unkey into eng-1597…
…-add-override-routes-in-unkeyratelimit-sdk-docs
- Loading branch information
Showing
31 changed files
with
874 additions
and
512 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export default function NoBreadcrumb() { | ||
return null; | ||
} |
This file was deleted.
Oops, something went wrong.
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,61 @@ | ||
import { trpc } from "@/lib/trpc/client"; | ||
import type { LogsTimeseriesDataPoint } from "@unkey/clickhouse/src/logs"; | ||
import { addMinutes, format } from "date-fns"; | ||
import { useLogSearchParams } from "../../query-state"; | ||
import { type TimeseriesGranularity, getTimeseriesGranularity } from "../../utils"; | ||
|
||
const roundToSecond = (timestamp: number) => Math.floor(timestamp / 1000) * 1000; | ||
|
||
const formatTimestamp = (value: string | number, granularity: TimeseriesGranularity) => { | ||
const date = new Date(value); | ||
const offset = new Date().getTimezoneOffset() * -1; | ||
const localDate = addMinutes(date, offset); | ||
|
||
switch (granularity) { | ||
case "perMinute": | ||
return format(localDate, "HH:mm:ss"); | ||
case "perHour": | ||
return format(localDate, "MMM d, HH:mm"); | ||
case "perDay": | ||
return format(localDate, "MMM d"); | ||
default: | ||
return format(localDate, "Pp"); | ||
} | ||
}; | ||
|
||
export const useFetchTimeseries = (initialTimeseries: LogsTimeseriesDataPoint[]) => { | ||
const { searchParams } = useLogSearchParams(); | ||
|
||
const filters = { | ||
host: searchParams.host, | ||
path: searchParams.path, | ||
method: searchParams.method, | ||
responseStatus: searchParams.responseStatus, | ||
}; | ||
|
||
const { | ||
startTime: rawStartTime, | ||
endTime: rawEndTime, | ||
granularity, | ||
} = getTimeseriesGranularity(searchParams.startTime, searchParams.endTime); | ||
|
||
const { data, isLoading } = trpc.logs.queryTimeseries.useQuery( | ||
{ | ||
startTime: roundToSecond(rawStartTime), | ||
endTime: roundToSecond(rawEndTime), | ||
...filters, | ||
}, | ||
{ | ||
refetchInterval: searchParams.endTime ? false : 10_000, | ||
initialData: initialTimeseries, | ||
}, | ||
); | ||
|
||
const timeseries = data.map((data) => ({ | ||
displayX: formatTimestamp(data.x, granularity), | ||
originalTimestamp: data.x, | ||
...data.y, | ||
})); | ||
|
||
return { timeseries, isLoading }; | ||
}; |
131 changes: 131 additions & 0 deletions
131
apps/dashboard/app/(app)/logs/components/charts/index.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,131 @@ | ||
"use client"; | ||
import { | ||
type ChartConfig, | ||
ChartContainer, | ||
ChartTooltip, | ||
ChartTooltipContent, | ||
} from "@/components/ui/chart"; | ||
import type { LogsTimeseriesDataPoint } from "@unkey/clickhouse/src/logs"; | ||
import { addMinutes, format } from "date-fns"; | ||
import { useEffect, useState } from "react"; | ||
import { Bar, BarChart, CartesianGrid, ResponsiveContainer, XAxis, YAxis } from "recharts"; | ||
import { useFetchTimeseries } from "./hooks"; | ||
|
||
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; | ||
|
||
const formatTimestampTooltip = (value: string | number) => { | ||
const date = new Date(value); | ||
const offset = new Date().getTimezoneOffset() * -1; | ||
const localDate = addMinutes(date, offset); | ||
return format(localDate, "dd MMM HH:mm:ss"); | ||
}; | ||
|
||
const calculateTickInterval = (dataLength: number, containerWidth: number) => { | ||
const pixelsPerTick = 80; // Adjust this value to control density | ||
const suggestedInterval = Math.ceil((dataLength * pixelsPerTick) / containerWidth); | ||
|
||
const intervals = [1, 2, 5, 10, 15, 30, 60]; | ||
return intervals.find((i) => i >= suggestedInterval) || intervals[intervals.length - 1]; | ||
}; | ||
|
||
export function LogsChart({ | ||
initialTimeseries, | ||
}: { | ||
initialTimeseries: LogsTimeseriesDataPoint[]; | ||
}) { | ||
const { timeseries } = useFetchTimeseries(initialTimeseries); | ||
const [tickInterval, setTickInterval] = useState(5); | ||
const [containerWidth, setContainerWidth] = useState(0); | ||
|
||
useEffect(() => { | ||
if (containerWidth > 0 && timeseries.length > 0) { | ||
const newInterval = calculateTickInterval(timeseries.length, containerWidth); | ||
setTickInterval(newInterval); | ||
} | ||
}, [timeseries.length, containerWidth]); | ||
|
||
const maxValue = Math.max( | ||
...timeseries.map((item) => (item.success || 0) + (item.warning || 0) + (item.error || 0)), | ||
); | ||
const yAxisMax = Math.ceil(maxValue * 1.1); | ||
|
||
return ( | ||
<div className="w-full h-[120px] p-1 rounded-lg bg-card"> | ||
<ResponsiveContainer | ||
width="100%" | ||
height="60px" | ||
onResize={(width) => setContainerWidth(width)} | ||
> | ||
<ChartContainer config={chartConfig}> | ||
<BarChart data={timeseries} barCategoryGap={4} maxBarSize={20}> | ||
<XAxis | ||
dataKey="displayX" | ||
tickLine={false} | ||
axisLine={false} | ||
className="font-mono" | ||
minTickGap={30} | ||
tick={{ | ||
fontSize: 11, | ||
fill: "hsl(var(--muted-foreground))", | ||
dy: 10, | ||
}} | ||
interval={tickInterval - 1} | ||
/> | ||
<CartesianGrid | ||
strokeDasharray="3 3" | ||
stroke="hsl(var(--border))" | ||
vertical={false} | ||
strokeOpacity={0.4} | ||
/> | ||
<ChartTooltip | ||
cursor={false} | ||
content={ | ||
<ChartTooltipContent | ||
className="rounded-lg shadow-lg border border-border" | ||
labelFormatter={(_, payload) => { | ||
const originalTimestamp = payload[0]?.payload?.originalTimestamp; | ||
return originalTimestamp ? ( | ||
<span className="font-mono text-muted-foreground text-xs"> | ||
{formatTimestampTooltip(originalTimestamp)} | ||
</span> | ||
) : ( | ||
"" | ||
); | ||
}} | ||
/> | ||
} | ||
/> | ||
<YAxis domain={[0, yAxisMax]} hide /> | ||
{["success", "warning", "error"].map((key, index) => ( | ||
<Bar | ||
key={key} | ||
dataKey={key} | ||
stackId="a" | ||
fill={`var(--color-${key})`} | ||
radius={[ | ||
index === 0 ? 2 : 0, | ||
index === 2 ? 2 : 0, | ||
index === 2 ? 2 : 0, | ||
index === 0 ? 2 : 0, | ||
]} | ||
/> | ||
))} | ||
</BarChart> | ||
</ChartContainer> | ||
</ResponsiveContainer> | ||
</div> | ||
); | ||
} |
Oops, something went wrong.