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:
arsam
2026-09-18 23:10:45 +03:30
parent 718450acc6
commit 93d46851be
+227
View File
@@ -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>
);
}