first commit
This commit is contained in:
32
frontend/src/components/realtime/HeroKpiGrid.tsx
Normal file
32
frontend/src/components/realtime/HeroKpiGrid.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import clsx from "clsx";
|
||||
import { Card } from "../common/Card";
|
||||
import { formatValue } from "../../lib/format";
|
||||
import type { HeroCard } from "../../types";
|
||||
|
||||
interface HeroKpiGridProps {
|
||||
cards: HeroCard[];
|
||||
}
|
||||
|
||||
const accents: Record<string, string> = {
|
||||
emerald: "from-emerald-400/15 to-emerald-500/5 ring-emerald-300/20",
|
||||
amber: "from-amber-400/15 to-amber-500/5 ring-amber-300/20",
|
||||
rose: "from-rose-400/15 to-rose-500/5 ring-rose-300/20",
|
||||
slate: "from-white/10 to-white/5 ring-white/10",
|
||||
};
|
||||
|
||||
export function HeroKpiGrid({ cards }: HeroKpiGridProps) {
|
||||
return (
|
||||
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
|
||||
{cards.map((card) => (
|
||||
<Card
|
||||
key={card.metric_id}
|
||||
className={clsx("overflow-hidden bg-gradient-to-br", accents[card.accent] ?? accents.slate)}
|
||||
>
|
||||
<div className="text-xs uppercase tracking-[0.22em] text-slate-400">{card.label}</div>
|
||||
<div className="mt-3 text-3xl font-semibold text-white">{formatValue(card.value, card.unit, 2)}</div>
|
||||
<div className="mt-3 text-sm text-slate-400">{card.subtitle}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
frontend/src/components/realtime/KpiStrip.tsx
Normal file
35
frontend/src/components/realtime/KpiStrip.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Card } from "../common/Card";
|
||||
import { formatValue } from "../../lib/format";
|
||||
import type { MetricValue } from "../../types";
|
||||
|
||||
interface KpiStripProps {
|
||||
items: Record<string, MetricValue>;
|
||||
}
|
||||
|
||||
const order = [
|
||||
"energy_today",
|
||||
"energy_yesterday",
|
||||
"today_vs_yesterday",
|
||||
"dc_power_total",
|
||||
"energy_total",
|
||||
];
|
||||
|
||||
export function KpiStrip({ items }: KpiStripProps) {
|
||||
return (
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
{order
|
||||
.filter((metricId) => items[metricId])
|
||||
.map((metricId) => {
|
||||
const metric = items[metricId];
|
||||
return (
|
||||
<Card key={metric.metric_id} className="bg-slate-950/35">
|
||||
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">{metric.label}</div>
|
||||
<div className="mt-3 text-2xl font-semibold text-white">
|
||||
{formatValue(metric.value, metric.unit, metric.precision)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
64
frontend/src/components/realtime/LiveHistoryChart.tsx
Normal file
64
frontend/src/components/realtime/LiveHistoryChart.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import type { EChartsOption } from "echarts";
|
||||
import { Card } from "../common/Card";
|
||||
import { EChart } from "../common/EChart";
|
||||
import type { HistoryPayload } from "../../types";
|
||||
|
||||
interface LiveHistoryChartProps {
|
||||
history?: HistoryPayload;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function LiveHistoryChart({ history, title = "Dane chwilowe" }: LiveHistoryChartProps) {
|
||||
const option: EChartsOption = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
backgroundColor: "rgba(2, 6, 23, 0.95)",
|
||||
borderColor: "rgba(255,255,255,0.08)",
|
||||
textStyle: { color: "#e2e8f0" },
|
||||
},
|
||||
legend: {
|
||||
top: 0,
|
||||
textStyle: { color: "#cbd5e1" },
|
||||
},
|
||||
grid: {
|
||||
left: 18,
|
||||
right: 16,
|
||||
top: 46,
|
||||
bottom: 24,
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
boundaryGap: false,
|
||||
axisLabel: { color: "#94a3b8" },
|
||||
axisLine: { lineStyle: { color: "rgba(255,255,255,0.08)" } },
|
||||
data: history?.series[0]?.points.map((point) =>
|
||||
new Date(point.timestamp).toLocaleTimeString("pl-PL", { hour: "2-digit", minute: "2-digit" })
|
||||
) ?? [],
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
axisLabel: { color: "#94a3b8" },
|
||||
splitLine: { lineStyle: { color: "rgba(255,255,255,0.06)" } },
|
||||
},
|
||||
series:
|
||||
history?.series.map((item) => ({
|
||||
name: `${item.label} (${item.unit})`,
|
||||
type: "line",
|
||||
smooth: true,
|
||||
showSymbol: false,
|
||||
lineStyle: { width: 3 },
|
||||
areaStyle: { opacity: 0.08 },
|
||||
data: item.points.map((point) => point.value),
|
||||
})) ?? [],
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
title={title}
|
||||
subtitle="Moc AC, moce stringow DC i opcjonalnie temperatura falownika w jednym widoku live"
|
||||
>
|
||||
<EChart option={option} className="h-[340px] w-full" />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
30
frontend/src/components/realtime/LiveStatusBoard.tsx
Normal file
30
frontend/src/components/realtime/LiveStatusBoard.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Badge } from "../common/Badge";
|
||||
import { Card } from "../common/Card";
|
||||
import { formatValue } from "../../lib/format";
|
||||
import type { MetricValue } from "../../types";
|
||||
|
||||
interface LiveStatusBoardProps {
|
||||
status: MetricValue[];
|
||||
}
|
||||
|
||||
export function LiveStatusBoard({ status }: LiveStatusBoardProps) {
|
||||
if (!status.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Stan systemu" subtitle="Temperatura falownika i kontrola swiezosci odczytu">
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{status.map((metric) => (
|
||||
<div key={metric.metric_id} className="rounded-2xl border border-white/10 bg-slate-950/30 p-4">
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div className="text-sm font-medium text-white">{metric.label}</div>
|
||||
<Badge tone={metric.status}>{metric.status}</Badge>
|
||||
</div>
|
||||
<div className="text-sm text-slate-300">{formatValue(metric.value, metric.unit, metric.precision)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
26
frontend/src/components/realtime/PhaseGrid.tsx
Normal file
26
frontend/src/components/realtime/PhaseGrid.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Card } from "../common/Card";
|
||||
import { ValuePair } from "../common/ValuePair";
|
||||
import type { SnapshotGroupRow } from "../../types";
|
||||
|
||||
interface PhaseGridProps {
|
||||
rows: SnapshotGroupRow[];
|
||||
}
|
||||
|
||||
export function PhaseGrid({ rows }: PhaseGridProps) {
|
||||
return (
|
||||
<Card title="Fazy AC" subtitle="Napiece, prady i moce pozorne na falowniku">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{rows.map((row) => (
|
||||
<div key={row.id} className="rounded-3xl border border-white/10 bg-slate-950/40 p-4">
|
||||
<div className="mb-4 text-sm font-semibold text-white">{row.label}</div>
|
||||
<div className="grid gap-3">
|
||||
<ValuePair metric={row.values.voltage} />
|
||||
<ValuePair metric={row.values.current} />
|
||||
<ValuePair metric={row.values.apparent_power} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
34
frontend/src/components/realtime/StringGrid.tsx
Normal file
34
frontend/src/components/realtime/StringGrid.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Card } from "../common/Card";
|
||||
import { ValuePair } from "../common/ValuePair";
|
||||
import type { SnapshotGroupRow } from "../../types";
|
||||
|
||||
interface StringGridProps {
|
||||
rows: SnapshotGroupRow[];
|
||||
}
|
||||
|
||||
const slotOrder = ["power", "voltage"] as const;
|
||||
|
||||
export function StringGrid({ rows }: StringGridProps) {
|
||||
return (
|
||||
<Card title="Stringi DC" subtitle="Widok automatycznie skaluje sie do liczby stringow i dostepnych metryk z config.py">
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{rows.map((row) => {
|
||||
const visibleSlots = slotOrder.filter((slot) => row.values[slot]);
|
||||
return (
|
||||
<div key={row.id} className="rounded-3xl border border-white/10 bg-slate-950/40 p-4">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="text-sm font-semibold text-white">{row.label}</div>
|
||||
<div className="text-xs uppercase tracking-[0.18em] text-slate-500">{row.id}</div>
|
||||
</div>
|
||||
<div className={`grid gap-3 ${visibleSlots.length > 1 ? "sm:grid-cols-2" : "sm:grid-cols-1"}`}>
|
||||
{visibleSlots.map((slot) => (
|
||||
<ValuePair key={slot} metric={row.values[slot]} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user