Files
inbuxa-admin/src/features/dashboard/components/DashboardChart.tsx
T
jcoffey-dev 3a33f3d514 Mark the files this fork changed (AGPL section 5(a))
Publishing the source is what asks for it: a modified version has to carry
prominent notices saying it was modified, with a date. These files already
added a Coffey Labs copyright line beside upstream's, which implies as much
without saying it; now they say it.

38 files, found by diffing against the merge base with upstream rather than
by guessing. Upstream's own notices are untouched. The build is unchanged.
2026-09-19 23:48:44 -07:00

258 lines
6.9 KiB
TypeScript

/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
* SPDX-FileCopyrightText: 2026 Coffey Labs
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*
* Modified by Coffey Labs in 2026 for INBUXA.
*/
import { useMemo, useRef, useState, useEffect } from 'react';
import {
LineChart,
Line,
AreaChart,
Area,
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
} from 'recharts';
function ChartSizedContainer({
height,
children,
}: {
height: number;
children: (width: number, height: number) => React.ReactNode;
}) {
const ref = useRef<HTMLDivElement>(null);
const [width, setWidth] = useState(0);
useEffect(() => {
const el = ref.current;
if (!el) return;
const ro = new ResizeObserver((entries) => {
for (const entry of entries) {
const w = entry.contentRect.width;
if (w > 0) setWidth(w);
}
});
ro.observe(el);
const w = el.getBoundingClientRect().width;
if (w > 0) setWidth(w);
return () => ro.disconnect();
}, []);
return (
<div ref={ref} style={{ height }}>
{width > 0 && children(width, height)}
</div>
);
}
import { Info } from 'lucide-react';
import { GoLink } from './GoLink';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Tooltip as UiTooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import { getChartColor } from '@/components/ui/chart';
import { ChartTooltipContent } from '@/components/ui/chart';
import type { Chart as ChartSchema } from '../types/schema';
import type { Metric, Period } from '../types/metrics';
import {
bucketize,
bucketTimestamps,
getBucketCount,
seriesBucketValue,
formatTimeTick,
formatValue,
} from '../helpers';
interface DashboardChartProps {
chart: ChartSchema;
historySamples: Metric[];
historyWindow: { from: Date; to: Date };
period: Period;
}
export function DashboardChart({ chart, historySamples, historyWindow, period }: DashboardChartProps) {
const { from, to } = historyWindow;
const bucketCount = getBucketCount(period);
const valueFormat = chart.valueFormat ?? 'number';
const data = useMemo(() => {
const buckets = bucketize(historySamples, from, to, bucketCount);
const timestamps = bucketTimestamps(from, to, bucketCount);
const points = timestamps.map((ts, i) => {
const point: Record<string, unknown> = {
time: ts.getTime(),
timeLabel: formatTimeTick(ts, period),
};
for (const series of chart.series) {
point[series.label] = seriesBucketValue(series, buckets[i]);
}
return point;
});
if (chart.stacked) {
const lastSeen: Record<string, number> = {};
for (const point of points) {
for (const series of chart.series) {
const v = point[series.label];
if (typeof v === 'number') {
lastSeen[series.label] = v;
} else {
point[series.label] = lastSeen[series.label] ?? 0;
}
}
}
}
return points;
}, [historySamples, from, to, bucketCount, chart.series, chart.stacked, period]);
const tickFormatter = (value: number) => formatValue(value, valueFormat);
const tooltipFormatter = (value: number) => formatValue(value, valueFormat);
const renderChart = (chartWidth: number, chartHeight: number) => {
const commonProps = {
data,
width: chartWidth,
height: chartHeight,
margin: { top: 5, right: 10, left: 10, bottom: 5 },
};
const seriesElements = chart.series.map((s, i) => {
const color = getChartColor(i);
const key = s.label;
switch (chart.kind) {
case 'line':
return (
<Line
key={key}
type="monotone"
dataKey={key}
stroke={color}
strokeWidth={2}
dot={false}
isAnimationActive={false}
connectNulls
/>
);
case 'area':
return (
<Area
key={key}
type="monotone"
dataKey={key}
stroke={color}
fill={color}
fillOpacity={0.3}
strokeWidth={2}
stackId={chart.stacked ? '1' : undefined}
isAnimationActive={false}
connectNulls
/>
);
case 'bar':
return (
<Bar
key={key}
dataKey={key}
fill={color}
stackId={chart.stacked ? '1' : undefined}
isAnimationActive={false}
/>
);
}
});
const axes = (
<>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="timeLabel"
tick={{ fontSize: 11 }}
className="text-muted-foreground"
tickLine={false}
axisLine={false}
/>
<YAxis
tickFormatter={tickFormatter}
tick={{ fontSize: 11 }}
className="text-muted-foreground"
tickLine={false}
axisLine={false}
width={60}
/>
<Tooltip
content={({ active, payload, label }) => (
<ChartTooltipContent
active={active}
payload={payload}
label={label as string}
formatter={tooltipFormatter}
/>
)}
/>
<Legend iconType="circle" iconSize={8} wrapperStyle={{ fontSize: '12px' }} />
</>
);
switch (chart.kind) {
case 'line':
return (
<LineChart {...commonProps}>
{axes}
{seriesElements}
</LineChart>
);
case 'area':
return (
<AreaChart {...commonProps}>
{axes}
{seriesElements}
</AreaChart>
);
case 'bar':
return (
<BarChart {...commonProps}>
{axes}
{seriesElements}
</BarChart>
);
}
};
return (
<Card>
<CardHeader className="pb-2">
<div className="flex items-center gap-2">
<CardTitle className="text-base">{chart.title}</CardTitle>
{chart.description && (
<TooltipProvider>
<UiTooltip>
<TooltipTrigger asChild>
<Info className="h-3.5 w-3.5 text-muted-foreground/60 cursor-help" />
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
<p className="text-xs">{chart.description}</p>
</TooltipContent>
</UiTooltip>
</TooltipProvider>
)}
<GoLink metrics={chart.series.flatMap((x) => x.metrics)} />
</div>
</CardHeader>
<CardContent>
<ChartSizedContainer height={288}>{(width, height) => renderChart(width, height)}</ChartSizedContainer>
</CardContent>
</Card>
);
}