Import source: src/components/features/charts
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
Area,
|
||||
AreaChart,
|
||||
Bar,
|
||||
BarChart,
|
||||
CartesianGrid,
|
||||
Legend,
|
||||
Tooltip,
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from "recharts";
|
||||
import type { MetricPoint } from "@/api/contracts/models";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useUiStore } from "@/stores/ui-store";
|
||||
|
||||
/**
|
||||
* Recharts needs concrete color strings, so resolve the active CSS variables
|
||||
* whenever the accent or theme preference changes.
|
||||
*/
|
||||
function useChartTheme() {
|
||||
const accent = useUiStore((state) => state.accent);
|
||||
const theme = useUiStore((state) => state.theme);
|
||||
const [colors, setColors] = useState({
|
||||
primary: "hsl(224 88% 53%)",
|
||||
accent: "hsl(30 100% 48%)",
|
||||
muted: "hsl(226 14% 44%)",
|
||||
grid: "hsl(222 24% 86% / 0.5)",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Defer one frame so the Providers effect has applied the new
|
||||
// data-accent / dark-class attributes before the variables are read.
|
||||
const frame = requestAnimationFrame(() => {
|
||||
const styles = getComputedStyle(document.documentElement);
|
||||
const read = (name: string) => `hsl(${styles.getPropertyValue(name).trim()})`;
|
||||
setColors({
|
||||
primary: read("--primary"),
|
||||
accent: read("--accent"),
|
||||
muted: read("--muted-foreground"),
|
||||
grid: `hsl(${styles.getPropertyValue("--border").trim()} / 0.5)`,
|
||||
});
|
||||
});
|
||||
return () => cancelAnimationFrame(frame);
|
||||
}, [accent, theme]);
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
function GlassTooltip({
|
||||
active,
|
||||
payload,
|
||||
label,
|
||||
}: {
|
||||
active?: boolean;
|
||||
payload?: Array<{ name: string; value: number; color: string }>;
|
||||
label?: string;
|
||||
}) {
|
||||
if (!active || !payload?.length) return null;
|
||||
return (
|
||||
<div className="glass-modal rounded-lg px-3 py-2 text-xs">
|
||||
<div className="mb-1 font-mono text-muted-foreground">{label?.slice(11, 16)}</div>
|
||||
{payload.map((entry) => (
|
||||
<div key={entry.name} className="flex items-center gap-2">
|
||||
<span className="h-2 w-2 rounded-full" style={{ background: entry.color }} />
|
||||
<span className="text-muted-foreground">{entry.name}</span>
|
||||
<span className="ml-auto pl-3 font-mono font-medium text-foreground">
|
||||
{Math.round(entry.value).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartFrame({
|
||||
children,
|
||||
}: {
|
||||
children: (size: { width: number; height: number }) => ReactNode;
|
||||
}) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [size, setSize] = useState({ width: 0, height: 0 });
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current;
|
||||
if (!element) return;
|
||||
const target = element;
|
||||
|
||||
function readSize() {
|
||||
const rect = target.getBoundingClientRect();
|
||||
setSize({
|
||||
width: Math.max(1, Math.floor(rect.width)),
|
||||
height: Math.max(1, Math.floor(rect.height)),
|
||||
});
|
||||
}
|
||||
|
||||
readSize();
|
||||
const observer = new ResizeObserver(readSize);
|
||||
observer.observe(element);
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<CardContent className="h-56 min-w-0 sm:h-64 lg:h-72">
|
||||
<div ref={ref} className="h-full min-w-0">
|
||||
{size.width > 1 && size.height > 1 ? (
|
||||
children(size)
|
||||
) : (
|
||||
<div className="skeleton h-full" />
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
);
|
||||
}
|
||||
|
||||
export function UsageChart({
|
||||
data,
|
||||
title = "Resource history",
|
||||
}: {
|
||||
data: MetricPoint[];
|
||||
title?: string;
|
||||
}) {
|
||||
const colors = useChartTheme();
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<ChartFrame>
|
||||
{({ width, height }) => (
|
||||
<AreaChart data={data} width={width} height={height}>
|
||||
<defs>
|
||||
<linearGradient id="usage-cpu" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors.primary} stopOpacity={0.35} />
|
||||
<stop offset="95%" stopColor={colors.primary} stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
<linearGradient id="usage-memory" x1="0" x2="0" y1="0" y2="1">
|
||||
<stop offset="5%" stopColor={colors.accent} stopOpacity={0.25} />
|
||||
<stop offset="95%" stopColor={colors.accent} stopOpacity={0.02} />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<CartesianGrid stroke={colors.grid} strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={(value: string) => value.slice(11, 16)}
|
||||
tick={{ fontSize: 11, fill: colors.muted }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11, fill: colors.muted }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
width={36}
|
||||
/>
|
||||
<Tooltip content={<GlassTooltip />} />
|
||||
<Legend iconType="circle" iconSize={8} />
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cpu"
|
||||
stroke={colors.primary}
|
||||
strokeWidth={2}
|
||||
fill="url(#usage-cpu)"
|
||||
name="CPU"
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="memory"
|
||||
stroke={colors.accent}
|
||||
strokeWidth={2}
|
||||
fill="url(#usage-memory)"
|
||||
name="Memory"
|
||||
/>
|
||||
</AreaChart>
|
||||
)}
|
||||
</ChartFrame>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export function TrafficChart({ data }: { data: MetricPoint[] }) {
|
||||
const colors = useChartTheme();
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Traffic and requests</CardTitle>
|
||||
</CardHeader>
|
||||
<ChartFrame>
|
||||
{({ width, height }) => (
|
||||
<BarChart data={data.slice(-12)} width={width} height={height}>
|
||||
<CartesianGrid stroke={colors.grid} strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
tickFormatter={(value: string) => value.slice(11, 16)}
|
||||
tick={{ fontSize: 11, fill: colors.muted }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
/>
|
||||
<YAxis
|
||||
tick={{ fontSize: 11, fill: colors.muted }}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
width={36}
|
||||
/>
|
||||
<Tooltip content={<GlassTooltip />} cursor={{ fill: colors.grid }} />
|
||||
<Legend iconType="circle" iconSize={8} />
|
||||
<Bar
|
||||
dataKey="bandwidth"
|
||||
fill={colors.primary}
|
||||
name="Bandwidth MB"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="requests"
|
||||
fill={colors.accent}
|
||||
name="Requests"
|
||||
radius={[4, 4, 0, 0]}
|
||||
/>
|
||||
</BarChart>
|
||||
)}
|
||||
</ChartFrame>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user