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