feature(dashboard,api): add timezone support

* feat(dashboard): add support for today, yesterday etc (timezones)

* fix(db): escape js dates

* fix(dashboard): ensure we support default timezone

* final fixes

* remove complete series and add sql with fill instead
This commit is contained in:
Carl-Gerhard Lindesvärd
2025-05-23 11:26:44 +02:00
committed by GitHub
parent 46bfeee131
commit 680727355b
48 changed files with 1817 additions and 758 deletions

View File

@@ -6,13 +6,20 @@ import { api } from '@/trpc/client';
import type { IChartData } from '@/trpc/client';
import { cn } from '@/utils/cn';
import { getChartColor } from '@/utils/theme';
import { isSameDay, isSameHour, isSameMonth } from 'date-fns';
import {
isFuture,
isSameDay,
isSameHour,
isSameMonth,
isSameWeek,
} from 'date-fns';
import { last } from 'ramda';
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import {
Area,
CartesianGrid,
ComposedChart,
Customized,
Legend,
Line,
ReferenceLine,
@@ -22,6 +29,7 @@ import {
YAxis,
} from 'recharts';
import { useDashedStroke, useStrokeDasharray } from '@/hooks/use-dashed-stroke';
import { useXAxisProps, useYAxisProps } from '../common/axis';
import { SolidToDashedGradient } from '../common/linear-gradient';
import { ReportChartTooltip } from '../common/report-chart-tooltip';
@@ -63,15 +71,20 @@ export function Chart({ data }: Props) {
const { series, setVisibleSeries } = useVisibleSeries(data);
const rechartData = useRechartDataModel(series);
// great care should be taken when computing lastIntervalPercent
// the expression below works for data.length - 1 equal intervals
// but if there are numeric x values in a "linear" axis, the formula
// should be updated to use those values
const lastIntervalPercent =
((rechartData.length - 2) * 100) / (rechartData.length - 1);
let dotIndex = undefined;
if (range === 'today') {
// Find closest index based on times
dotIndex = rechartData.findIndex((item) => {
return isSameHour(item.date, new Date());
});
}
const lastSerieDataItem = last(series[0]?.data || [])?.date || new Date();
const useDashedLastLine = (() => {
if (range === 'today') {
return true;
}
if (interval === 'hour') {
return isSameHour(lastSerieDataItem, new Date());
}
@@ -84,9 +97,18 @@ export function Chart({ data }: Props) {
return isSameMonth(lastSerieDataItem, new Date());
}
if (interval === 'week') {
return isSameWeek(lastSerieDataItem, new Date());
}
return false;
})();
const { getStrokeDasharray, calcStrokeDasharray, handleAnimationEnd } =
useDashedStroke({
dotIndex,
});
const CustomLegend = useCallback(() => {
return (
<div className="flex flex-wrap justify-center gap-x-4 gap-y-1 text-xs mt-4 -mb-2">
@@ -117,6 +139,13 @@ export function Chart({ data }: Props) {
<div className={cn('h-full w-full', isEditMode && 'card p-4')}>
<ResponsiveContainer>
<ComposedChart data={rechartData}>
<Customized component={calcStrokeDasharray} />
<Line
dataKey="calcStrokeDasharray"
legendType="none"
animationDuration={0}
onAnimationEnd={handleAnimationEnd}
/>
<CartesianGrid
strokeDasharray="3 3"
horizontal={true}
@@ -166,13 +195,6 @@ export function Chart({ data }: Props) {
/>
</linearGradient>
)}
{useDashedLastLine && (
<SolidToDashedGradient
percentage={lastIntervalPercent}
baseColor={color}
id={`stroke${color}`}
/>
)}
</defs>
<Line
dot={isAreaStyle && dataLength <= 8}
@@ -181,7 +203,12 @@ export function Chart({ data }: Props) {
isAnimationActive={false}
strokeWidth={2}
dataKey={`${serie.id}:count`}
stroke={useDashedLastLine ? `url(#stroke${color})` : color}
stroke={color}
strokeDasharray={
useDashedLastLine
? getStrokeDasharray(`${serie.id}:count`)
: undefined
}
// Use for legend
fill={color}
/>