feat: dashboard-page
ci-front / build (push) Successful in 2m7s

This commit is contained in:
nikita
2026-04-04 16:53:12 +03:00
parent 55cb214458
commit 78f35f6811
11 changed files with 1221 additions and 0 deletions
@@ -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>
);
};