@@ -0,0 +1,299 @@
|
||||
// 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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user