300 lines
8.5 KiB
TypeScript
300 lines
8.5 KiB
TypeScript
// modules/dashboard/components/ChartWidget.tsx
|
||
import React from "react";
|
||
import {
|
||
LineChart,
|
||
Line,
|
||
BarChart,
|
||
Bar,
|
||
AreaChart,
|
||
Area,
|
||
PieChart,
|
||
Pie,
|
||
Cell,
|
||
XAxis,
|
||
YAxis,
|
||
CartesianGrid,
|
||
Tooltip,
|
||
ResponsiveContainer,
|
||
Legend,
|
||
} from "recharts";
|
||
import {
|
||
FaChartLine,
|
||
FaChartBar,
|
||
FaChartArea,
|
||
FaChartPie,
|
||
FaCog,
|
||
FaEye,
|
||
FaEyeSlash,
|
||
} from "react-icons/fa";
|
||
import { motion } from "framer-motion";
|
||
import type { ChartWidget as ChartWidgetType, MetricData } from "../types";
|
||
|
||
interface ChartWidgetProps {
|
||
widget: ChartWidgetType;
|
||
data: MetricData[];
|
||
onEdit: () => void;
|
||
onToggleVisibility: () => void;
|
||
}
|
||
|
||
// Все возможные уровни логов (метрики)
|
||
const METRICS = ["INFO", "WARN", "ERROR", "DEBUG"];
|
||
|
||
// Цвета для каждой метрики
|
||
const METRIC_COLORS: Record<string, string> = {
|
||
INFO: "#10b981", // зеленый
|
||
WARN: "#f59e0b", // оранжевый
|
||
ERROR: "#ef4444", // красный
|
||
DEBUG: "#3b82f6", // синий
|
||
};
|
||
|
||
export const ChartWidget: React.FC<ChartWidgetProps> = ({
|
||
widget,
|
||
data,
|
||
onEdit,
|
||
onToggleVisibility,
|
||
}) => {
|
||
const renderChart = () => {
|
||
if (!data || !Array.isArray(data) || data.length === 0) {
|
||
return (
|
||
<div className="flex items-center justify-center h-full">
|
||
<span className="text-[10px] text-tertiary">Нет данных</span>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const normalizedData = data.map((point) => {
|
||
const normalized: MetricData = { timestamp: point.timestamp };
|
||
METRICS.forEach((metric) => {
|
||
normalized[metric] = point[metric] || 0;
|
||
});
|
||
return normalized;
|
||
});
|
||
|
||
const commonProps = {
|
||
data: normalizedData,
|
||
margin: { top: 5, right: 10, left: 0, bottom: 5 },
|
||
};
|
||
|
||
switch (widget.type) {
|
||
case "line":
|
||
return (
|
||
<LineChart {...commonProps}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||
<XAxis
|
||
dataKey="timestamp"
|
||
stroke="#64748b"
|
||
tick={{ fontSize: 9 }}
|
||
interval={Math.floor(normalizedData.length / 5)}
|
||
/>
|
||
<YAxis stroke="#64748b" tick={{ fontSize: 9 }} width={30} />
|
||
<Tooltip
|
||
contentStyle={{
|
||
backgroundColor: "#1e293b",
|
||
border: "1px solid #334155",
|
||
borderRadius: "6px",
|
||
fontSize: "10px",
|
||
}}
|
||
labelStyle={{ color: "#fff" }}
|
||
/>
|
||
<Legend
|
||
wrapperStyle={{ fontSize: "10px" }}
|
||
verticalAlign="top"
|
||
height={25}
|
||
/>
|
||
{METRICS.map((metric) => (
|
||
<Line
|
||
key={metric}
|
||
type="monotone"
|
||
dataKey={metric}
|
||
stroke={METRIC_COLORS[metric]}
|
||
strokeWidth={2}
|
||
dot={false}
|
||
name={metric}
|
||
/>
|
||
))}
|
||
</LineChart>
|
||
);
|
||
|
||
case "bar":
|
||
return (
|
||
<BarChart {...commonProps}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||
<XAxis
|
||
dataKey="timestamp"
|
||
stroke="#64748b"
|
||
tick={{ fontSize: 9 }}
|
||
interval={Math.floor(normalizedData.length / 5)}
|
||
/>
|
||
<YAxis stroke="#64748b" tick={{ fontSize: 9 }} width={30} />
|
||
<Tooltip
|
||
contentStyle={{
|
||
backgroundColor: "#1e293b",
|
||
border: "1px solid #334155",
|
||
borderRadius: "6px",
|
||
fontSize: "10px",
|
||
}}
|
||
labelStyle={{ color: "#fff" }}
|
||
/>
|
||
<Legend
|
||
wrapperStyle={{ fontSize: "10px" }}
|
||
verticalAlign="top"
|
||
height={25}
|
||
/>
|
||
{METRICS.map((metric) => (
|
||
<Bar
|
||
key={metric}
|
||
dataKey={metric}
|
||
fill={METRIC_COLORS[metric]}
|
||
radius={[2, 2, 0, 0]}
|
||
name={metric}
|
||
/>
|
||
))}
|
||
</BarChart>
|
||
);
|
||
|
||
case "area":
|
||
return (
|
||
<AreaChart {...commonProps}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||
<XAxis
|
||
dataKey="timestamp"
|
||
stroke="#64748b"
|
||
tick={{ fontSize: 9 }}
|
||
interval={Math.floor(normalizedData.length / 5)}
|
||
/>
|
||
<YAxis stroke="#64748b" tick={{ fontSize: 9 }} width={30} />
|
||
<Tooltip
|
||
contentStyle={{
|
||
backgroundColor: "#1e293b",
|
||
border: "1px solid #334155",
|
||
borderRadius: "6px",
|
||
fontSize: "10px",
|
||
}}
|
||
labelStyle={{ color: "#fff" }}
|
||
/>
|
||
<Legend
|
||
wrapperStyle={{ fontSize: "10px" }}
|
||
verticalAlign="top"
|
||
height={25}
|
||
/>
|
||
{METRICS.map((metric) => (
|
||
<Area
|
||
key={metric}
|
||
type="monotone"
|
||
dataKey={metric}
|
||
stroke={METRIC_COLORS[metric]}
|
||
fill={METRIC_COLORS[metric]}
|
||
fillOpacity={0.2}
|
||
name={metric}
|
||
/>
|
||
))}
|
||
</AreaChart>
|
||
);
|
||
|
||
case "pie":
|
||
// Для круговой диаграммы берем последнюю точку
|
||
const lastPoint = normalizedData[normalizedData.length - 1];
|
||
const pieData = METRICS.map((metric) => ({
|
||
name: metric,
|
||
value: lastPoint[metric] || 0,
|
||
})).filter((item) => Number(item.value) > 0);
|
||
|
||
return (
|
||
<PieChart>
|
||
<Pie
|
||
data={pieData}
|
||
cx="50%"
|
||
cy="50%"
|
||
innerRadius={40}
|
||
outerRadius={55}
|
||
paddingAngle={3}
|
||
dataKey="value"
|
||
nameKey="name"
|
||
>
|
||
{pieData.map((entry, index) => (
|
||
<Cell key={`cell-${index}`} fill={METRIC_COLORS[entry.name]} />
|
||
))}
|
||
</Pie>
|
||
<Tooltip
|
||
contentStyle={{
|
||
backgroundColor: "#1e293b",
|
||
border: "1px solid #334155",
|
||
borderRadius: "6px",
|
||
fontSize: "10px",
|
||
}}
|
||
labelStyle={{ color: "#fff" }}
|
||
/>
|
||
<Legend
|
||
wrapperStyle={{ fontSize: "10px" }}
|
||
layout="vertical"
|
||
verticalAlign="middle"
|
||
align="right"
|
||
/>
|
||
</PieChart>
|
||
);
|
||
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const getIcon = () => {
|
||
switch (widget.type) {
|
||
case "line":
|
||
return <FaChartLine size={10} />;
|
||
case "bar":
|
||
return <FaChartBar size={10} />;
|
||
case "area":
|
||
return <FaChartArea size={10} />;
|
||
case "pie":
|
||
return <FaChartPie size={10} />;
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
return (
|
||
<motion.div
|
||
layout
|
||
initial={{ opacity: 0, scale: 0.95 }}
|
||
animate={{ opacity: 1, scale: 1 }}
|
||
exit={{ opacity: 0, scale: 0.95 }}
|
||
className={`bg-secondary rounded-lg border border-primary p-2 transition-all ${!widget.visible ? "opacity-50" : ""}`}
|
||
>
|
||
<div className="flex items-center justify-between mb-1 px-1">
|
||
<div className="flex items-center gap-1">
|
||
<span className="text-tertiary">{getIcon()}</span>
|
||
<h3 className="text-[11px] font-medium text-primary">
|
||
{widget.title}
|
||
</h3>
|
||
</div>
|
||
<div className="flex gap-0.5">
|
||
<button
|
||
onClick={onToggleVisibility}
|
||
className="p-0.5 hover:bg-tertiary rounded transition-colors cursor-pointer"
|
||
title={widget.visible ? "Скрыть" : "Показать"}
|
||
>
|
||
{widget.visible ? (
|
||
<FaEye size={9} className="text-tertiary" />
|
||
) : (
|
||
<FaEyeSlash size={9} className="text-tertiary" />
|
||
)}
|
||
</button>
|
||
<button
|
||
onClick={onEdit}
|
||
className="p-0.5 hover:bg-tertiary rounded transition-colors cursor-pointer"
|
||
title="Настройки"
|
||
>
|
||
<FaCog size={9} className="text-tertiary" />
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<div className="h-40">
|
||
<ResponsiveContainer width="100%" height="100%">
|
||
{renderChart()}
|
||
</ResponsiveContainer>
|
||
</div>
|
||
</motion.div>
|
||
);
|
||
};
|