165 lines
7.2 KiB
TypeScript
165 lines
7.2 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";
|
|
import { Card, CardHeader, CardTitle, CardContent } from "./ui/card";
|
|
import { Activity } from "lucide-react";
|
|
import { cn } from "../lib/utils";
|
|
import type { ActivityData } from "@shared/modules/dashboard/dashboard.types";
|
|
|
|
interface ActivityChartProps {
|
|
className?: string;
|
|
data?: ActivityData[];
|
|
}
|
|
|
|
export function ActivityChart({ className, data: providedData }: ActivityChartProps) {
|
|
const [data, setData] = useState<any[]>([]); // using any[] for the displayTime extension
|
|
const [isLoading, setIsLoading] = useState(!providedData);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (providedData) {
|
|
// Process provided data
|
|
const formatted = providedData.map((item) => ({
|
|
...item,
|
|
displayTime: new Date(item.hour).getHours().toString().padStart(2, '0') + ':00',
|
|
}));
|
|
setData(formatted);
|
|
return;
|
|
}
|
|
|
|
let mounted = true;
|
|
|
|
async function fetchActivity() {
|
|
try {
|
|
const response = await fetch("/api/stats/activity");
|
|
if (!response.ok) throw new Error("Failed to fetch activity data");
|
|
const result = await response.json();
|
|
|
|
if (mounted) {
|
|
// Normalize data: ensure we have 24 hours format
|
|
// The API returns { hour: ISOString, commands: number, transactions: number }
|
|
// We want to format hour to readable time
|
|
const formatted = result.map((item: ActivityData) => ({
|
|
...item,
|
|
displayTime: new Date(item.hour).getHours().toString().padStart(2, '0') + ':00',
|
|
}));
|
|
|
|
// Sort by time just in case, though API should handle it
|
|
setData(formatted);
|
|
|
|
// Only set loading to false on the first load to avoid flickering
|
|
setIsLoading(false);
|
|
}
|
|
} catch (err) {
|
|
if (mounted) {
|
|
console.error(err);
|
|
setError("Failed to load activity data");
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
fetchActivity();
|
|
|
|
// Refresh every 60 seconds
|
|
const interval = setInterval(fetchActivity, 60000);
|
|
|
|
return () => {
|
|
mounted = false;
|
|
clearInterval(interval);
|
|
};
|
|
}, [providedData]);
|
|
|
|
if (error) {
|
|
return (
|
|
<Card className={cn("glass-card", className)}>
|
|
<CardContent className="flex items-center justify-center h-[300px] text-destructive">
|
|
{error}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Card className={cn("glass-card overflow-hidden", className)}>
|
|
<CardHeader className="pb-2">
|
|
<div className="flex items-center gap-2">
|
|
<Activity className="w-5 h-5 text-primary" />
|
|
<CardTitle>24h Activity</CardTitle>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="h-[250px] w-full">
|
|
{isLoading ? (
|
|
<div className="h-full w-full flex items-center justify-center">
|
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
|
</div>
|
|
) : (
|
|
<ResponsiveContainer width="100%" height="100%">
|
|
<AreaChart
|
|
data={data}
|
|
margin={{
|
|
top: 10,
|
|
right: 10,
|
|
left: 0,
|
|
bottom: 0,
|
|
}}
|
|
>
|
|
<defs>
|
|
<linearGradient id="colorCommands" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="var(--primary)" stopOpacity={0.8} />
|
|
<stop offset="95%" stopColor="var(--primary)" stopOpacity={0} />
|
|
</linearGradient>
|
|
<linearGradient id="colorTx" x1="0" y1="0" x2="0" y2="1">
|
|
<stop offset="5%" stopColor="var(--secondary)" stopOpacity={0.8} />
|
|
<stop offset="95%" stopColor="var(--secondary)" stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="var(--border)" />
|
|
<XAxis
|
|
dataKey="displayTime"
|
|
stroke="var(--muted-foreground)"
|
|
fontSize={12}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
/>
|
|
<YAxis
|
|
stroke="var(--muted-foreground)"
|
|
fontSize={12}
|
|
tickLine={false}
|
|
axisLine={false}
|
|
tickFormatter={(value) => `${value}`}
|
|
/>
|
|
<Tooltip
|
|
contentStyle={{
|
|
backgroundColor: "var(--card)",
|
|
borderColor: "var(--border)",
|
|
borderRadius: "calc(var(--radius) + 2px)",
|
|
color: "var(--foreground)"
|
|
}}
|
|
itemStyle={{ color: "var(--foreground)" }}
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="commands"
|
|
name="Commands"
|
|
stroke="var(--primary)"
|
|
fillOpacity={1}
|
|
fill="url(#colorCommands)"
|
|
/>
|
|
<Area
|
|
type="monotone"
|
|
dataKey="transactions"
|
|
name="Transactions"
|
|
stroke="var(--secondary)"
|
|
fillOpacity={1}
|
|
fill="url(#colorTx)"
|
|
/>
|
|
</AreaChart>
|
|
</ResponsiveContainer>
|
|
)}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|