fix: a lot of minor improvements for dashboard

This commit is contained in:
Carl-Gerhard Lindesvärd
2025-11-07 12:28:54 +01:00
parent 5b1c582023
commit c762bd7c95
19 changed files with 591 additions and 388 deletions

View File

@@ -4,6 +4,9 @@ import { useQuery } from '@tanstack/react-query';
import { useNumber } from '@/hooks/use-numer-formatter';
import { getChartColor } from '@/utils/theme';
import * as Portal from '@radix-ui/react-portal';
import { bind } from 'bind-event-listener';
import throttle from 'lodash.throttle';
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import {
@@ -18,7 +21,6 @@ import {
} from 'recharts';
import { BarShapeBlue } from '../charts/common-bar';
import { SerieIcon } from '../report-chart/common/serie-icon';
interface OverviewLiveHistogramProps {
projectId: string;
}
@@ -120,26 +122,38 @@ function Wrapper({ children, count, icons }: WrapperProps) {
// Custom tooltip component that uses portals to escape overflow hidden
const CustomTooltip = ({ active, payload, coordinate }: any) => {
const [tooltipContainer] = useState(() => document.createElement('div'));
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const number = useNumber();
const [position, setPosition] = useState<{ x: number; y: number } | null>(
null,
);
const inactive = !active || !payload?.length;
useEffect(() => {
document.body.appendChild(tooltipContainer);
const handleMouseMove = (e: MouseEvent) => {
setMousePosition({ x: e.clientX, y: e.clientY });
};
document.addEventListener('mousemove', handleMouseMove);
const setPositionThrottled = throttle(setPosition, 50);
const unsubMouseMove = bind(window, {
type: 'mousemove',
listener(event) {
if (!inactive) {
setPositionThrottled({ x: event.clientX, y: event.clientY + 20 });
}
},
});
const unsubDragEnter = bind(window, {
type: 'pointerdown',
listener() {
setPosition(null);
},
});
return () => {
if (document.body.contains(tooltipContainer)) {
document.body.removeChild(tooltipContainer);
}
document.removeEventListener('mousemove', handleMouseMove);
unsubMouseMove();
unsubDragEnter();
};
}, [tooltipContainer]);
}, [inactive]);
if (inactive) {
return null;
}
if (!active || !payload || !payload.length) {
return null;
@@ -147,44 +161,31 @@ const CustomTooltip = ({ active, payload, coordinate }: any) => {
const data = payload[0].payload;
// Smart positioning to avoid going out of bounds
const tooltipWidth = 220; // min-w-[220px] to accommodate referrers
const tooltipHeight = 120; // approximate height with referrers
const offset = 10;
const tooltipWidth = 200;
const correctXPosition = (x: number | undefined) => {
if (!x) {
return undefined;
}
let left = mousePosition.x + offset;
let top = mousePosition.y - offset;
const screenWidth = window.innerWidth;
const newX = x;
// Check if tooltip would go off the right edge
if (left + tooltipWidth > window.innerWidth) {
left = mousePosition.x - tooltipWidth - offset;
}
if (newX + tooltipWidth > screenWidth) {
return screenWidth - tooltipWidth;
}
return newX;
};
// Check if tooltip would go off the left edge
if (left < 0) {
left = offset;
}
// Check if tooltip would go off the top edge
if (top < 0) {
top = mousePosition.y + offset;
}
// Check if tooltip would go off the bottom edge
if (top + tooltipHeight > window.innerHeight) {
top = window.innerHeight - tooltipHeight - offset;
}
const tooltipContent = (
<div
className="flex min-w-[220px] flex-col gap-2 rounded-xl border bg-background/80 p-3 shadow-xl backdrop-blur-sm"
return (
<Portal.Portal
style={{
position: 'fixed',
top,
left,
pointerEvents: 'none',
top: position?.y,
left: correctXPosition(position?.x),
zIndex: 1000,
width: tooltipWidth,
}}
className="bg-background/80 p-3 rounded-md border shadow-xl backdrop-blur-sm"
>
<div className="flex justify-between gap-8 text-muted-foreground">
<div>{data.time}</div>
@@ -234,8 +235,6 @@ const CustomTooltip = ({ active, payload, coordinate }: any) => {
</div>
)}
</React.Fragment>
</div>
</Portal.Portal>
);
return createPortal(tooltipContent, tooltipContainer);
};

View File

@@ -17,11 +17,13 @@ import { last, omit } from 'ramda';
import React, { useState } from 'react';
import {
Bar,
BarChart,
CartesianGrid,
ComposedChart,
Customized,
Line,
LineChart,
ReferenceLine,
ResponsiveContainer,
XAxis,
YAxis,
@@ -128,6 +130,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
}))}
active={metric === index}
isLoading={overviewQuery.isLoading}
inverted={title.inverted}
/>
))}
@@ -179,6 +182,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
interval={interval}
data={data}
chartType={chartType}
projectId={projectId}
/>
</div>
</div>
@@ -244,15 +248,33 @@ function Chart({
interval,
data,
chartType,
projectId,
}: {
activeMetric: (typeof TITLES)[number];
interval: IInterval;
data: RouterOutputs['overview']['stats']['series'];
chartType: 'bars' | 'lines';
projectId: string;
}) {
const xAxisProps = useXAxisProps({ interval });
const yAxisProps = useYAxisProps();
const [activeBar, setActiveBar] = useState(-1);
const { range, startDate, endDate } = useOverviewOptions();
const trpc = useTRPC();
const references = useQuery(
trpc.reference.getChartReferences.queryOptions(
{
projectId,
startDate,
endDate,
range,
},
{
staleTime: 1000 * 60 * 10,
},
),
);
// Line chart specific logic
let dotIndex = undefined;
@@ -372,6 +394,22 @@ function Chart({
r: 4,
}}
/>
{references.data?.map((ref) => (
<ReferenceLine
key={ref.id}
x={ref.date.getTime()}
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
strokeDasharray={'3 3'}
label={{
value: ref.title,
position: 'centerTop',
fill: '#334155',
fontSize: 12,
}}
fontSize={10}
/>
))}
</LineChart>
</ResponsiveContainer>
</TooltipProvider>
@@ -382,7 +420,7 @@ function Chart({
return (
<TooltipProvider metric={activeMetric} interval={interval}>
<ResponsiveContainer width="100%" height="100%">
<ComposedChart
<BarChart
data={data}
margin={{ top: 0, right: 0, left: 0, bottom: 10 }}
onMouseMove={(e) => {
@@ -426,7 +464,23 @@ function Chart({
<BarShapeBlue isActive={activeBar === props.index} {...props} />
)}
/>
</ComposedChart>
{references.data?.map((ref) => (
<ReferenceLine
key={ref.id}
x={ref.date.getTime()}
stroke={'oklch(from var(--foreground) l c h / 0.1)'}
strokeDasharray={'3 3'}
label={{
value: ref.title,
position: 'centerTop',
fill: '#334155',
fontSize: 12,
}}
fontSize={10}
/>
))}
</BarChart>
</ResponsiveContainer>
</TooltipProvider>
);