Skip to content

Commit

Permalink
feat: add timezone support to logs explorer chart (built with Chartjs) (
Browse files Browse the repository at this point in the history
#6566)

* feat: display timezone adjusted time range in time picker

* fix: make x axis and tooltip time format consistent in uPlot graphs

* fix: open the timepicker on clicking the input after closing the timezone picker by clicking outside

* feat: custom hook for formatting timezone

* feat: add timezone support to traces explorer timestamp column

* feat: add timezone support to saved views

* chore: improve timezone formatter custom hook

* feat: add support for timezone adjusted timestamp in raw log view (logs explorer -> list view)

* feat: add support for timezone adjusted timestamp in log table view (logs explorer -> list view)

* feat: add support for timezone adjusted timestamp in log default view (logs explorer -> list view)

* feat: add support for timezone adjusted timestamp in log details side pane

* feat: add support for timezone adjusted timestamp in pipeline pages

* feat: add support for timezone in dashboard list

* feat: add support for timezone adjusted created/updated at in alert rules list page

* feat: add support for timezone adjusted created at in timeline table of alert history page

* feat: add support for timezone adjusted Firing Since in triggered alerts page

* feat: add support for timezone adjusted Timestamp for List Panel of dashboard

* feat: add support for timezone adjusted First/Last Seen in exceptions list page

* feat: add support for timezone adjusted date in exception details page

* feat: add support for timezone adjusted Valid From/To in History tab of Licences

* chore: rename formatTimestamp -> formatTimezoneAdjustedTimestamp

* feat: add timezone support to logs explorer chart (built with Chartjs)

* chore: remove unnecessary chartjs-adapter-date-fns import

* chore: improve cache key

* chore: make clearCacheEntries DRYer

* chore: add docstring to formatTimezoneAdjustedTimestamp
  • Loading branch information
ahmadshaheer authored Dec 3, 2024
1 parent 75b6735 commit a0a611c
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 17 deletions.
34 changes: 34 additions & 0 deletions frontend/src/components/Graph/index.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
_adapters,
BarController,
BarElement,
CategoryScale,
Expand All @@ -18,8 +19,10 @@ import {
} from 'chart.js';
import annotationPlugin from 'chartjs-plugin-annotation';
import { generateGridTitle } from 'container/GridPanelSwitch/utils';
import dayjs from 'dayjs';
import { useIsDarkMode } from 'hooks/useDarkMode';
import isEqual from 'lodash-es/isEqual';
import { useTimezone } from 'providers/Timezone';
import {
forwardRef,
memo,
Expand Down Expand Up @@ -62,6 +65,17 @@ Chart.register(

Tooltip.positioners.custom = TooltipPositionHandler;

// Map of Chart.js time formats to dayjs format strings
const formatMap = {
'HH:mm:ss': 'HH:mm:ss',
'HH:mm': 'HH:mm',
'MM/DD HH:mm': 'MM/DD HH:mm',
'MM/dd HH:mm': 'MM/DD HH:mm',
'MM/DD': 'MM/DD',
'YY-MM': 'YY-MM',
YY: 'YY',
};

const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
(
{
Expand All @@ -80,11 +94,13 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
dragSelectColor,
},
ref,
// eslint-disable-next-line sonarjs/cognitive-complexity
): JSX.Element => {
const nearestDatasetIndex = useRef<null | number>(null);
const chartRef = useRef<HTMLCanvasElement>(null);
const isDarkMode = useIsDarkMode();
const gridTitle = useMemo(() => generateGridTitle(title), [title]);
const { timezone } = useTimezone();

const currentTheme = isDarkMode ? 'dark' : 'light';
const xAxisTimeUnit = useXAxisTimeUnit(data); // Computes the relevant time unit for x axis by analyzing the time stamp data
Expand Down Expand Up @@ -112,6 +128,22 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
return 'rgba(231,233,237,0.8)';
}, [currentTheme]);

// Override Chart.js date adapter to use dayjs with timezone support
useEffect(() => {
_adapters._date.override({
format(time: number | Date, fmt: string) {
const dayjsTime = dayjs(time).tz(timezone?.value);
const format = formatMap[fmt as keyof typeof formatMap];
if (!format) {
console.warn(`Missing datetime format for ${fmt}`);
return dayjsTime.format('YYYY-MM-DD HH:mm:ss'); // fallback format
}

return dayjsTime.format(format);
},
});
}, [timezone]);

const buildChart = useCallback(() => {
if (lineChartRef.current !== undefined) {
lineChartRef.current.destroy();
Expand All @@ -132,6 +164,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
);

const chartHasData = hasData(data);
Expand Down Expand Up @@ -166,6 +199,7 @@ const Graph = forwardRef<ToggleGraphProps | undefined, GraphProps>(
isStacked,
onClickHandler,
data,
timezone,
name,
type,
]);
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/Graph/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Chart, ChartConfiguration, ChartData, Color } from 'chart.js';
import * as chartjsAdapter from 'chartjs-adapter-date-fns';
import { Timezone } from 'components/CustomTimePicker/timezoneUtils';
import dayjs from 'dayjs';
import { MutableRefObject } from 'react';

Expand Down Expand Up @@ -50,6 +51,7 @@ export const getGraphOptions = (
isStacked: boolean | undefined,
onClickHandler: GraphOnClickHandler | undefined,
data: ChartData,
timezone: Timezone,
// eslint-disable-next-line sonarjs/cognitive-complexity
): CustomChartOptions => ({
animation: {
Expand Down Expand Up @@ -97,7 +99,7 @@ export const getGraphOptions = (
callbacks: {
title(context): string | string[] {
const date = dayjs(context[0].parsed.x);
return date.format('MMM DD, YYYY, HH:mm:ss');
return date.tz(timezone?.value).format('MMM DD, YYYY, HH:mm:ss');
},
label(context): string | string[] {
let label = context.dataset.label || '';
Expand Down
37 changes: 21 additions & 16 deletions frontend/src/hooks/useTimezoneFormatter/useTimezoneFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,24 +39,39 @@ function useTimezoneFormatter({
cache.clear();
}, [cache, userTimezone]);

const clearExpiredEntries = useCallback(() => {
const clearCacheEntries = useCallback(() => {
if (cache.size <= CACHE_SIZE_LIMIT) return;

// Sort entries by timestamp (oldest first)
const sortedEntries = Array.from(cache.entries()).sort(
(a, b) => a[1].timestamp - b[1].timestamp,
);

// Calculate how many entries to remove
const entriesToRemove = Math.floor(cache.size * CACHE_CLEANUP_PERCENTAGE);
// Calculate how many entries to remove (50% or overflow, whichever is larger)
const entriesToRemove = Math.max(
Math.floor(cache.size * CACHE_CLEANUP_PERCENTAGE),
cache.size - CACHE_SIZE_LIMIT,
);

// Remove oldest entries
sortedEntries.slice(0, entriesToRemove).forEach(([key]) => cache.delete(key));
}, [cache]);

/**
* Formats a timestamp with the user's timezone and caches the result
* @param {TimestampInput} input - The timestamp to format (string, number, or Date)
* @param {string} [format='YYYY-MM-DD HH:mm:ss'] - The desired output format
* @returns {string} The formatted timestamp string in the user's timezone
* @example
* // Input: UTC timestamp
* // User timezone: 'UTC - 4'
* // Returns: "2024-03-14 15:30:00"
* formatTimezoneAdjustedTimestamp('2024-03-14T19:30:00Z')
*/
const formatTimezoneAdjustedTimestamp = useCallback(
(input: TimestampInput, format = 'YYYY-MM-DD HH:mm:ss'): string => {
const cacheKey = `${input}_${format}_${userTimezone?.value}`;
const timestamp = dayjs(input).valueOf();
const cacheKey = `${timestamp}_${userTimezone?.value}`;

// Check cache first
const cachedValue = cache.get(cacheKey);
Expand All @@ -74,22 +89,12 @@ function useTimezoneFormatter({

// Clear expired entries and enforce size limit
if (cache.size > CACHE_SIZE_LIMIT) {
clearExpiredEntries();

// If still over limit, remove oldest entries
const entriesToDelete = cache.size - CACHE_SIZE_LIMIT;
if (entriesToDelete > 0) {
const entries = Array.from(cache.entries());
entries
.sort((a, b) => a[1].timestamp - b[1].timestamp)
.slice(0, entriesToDelete)
.forEach(([key]) => cache.delete(key));
}
clearCacheEntries();
}

return formattedValue;
},
[cache, clearExpiredEntries, userTimezone],
[cache, clearCacheEntries, userTimezone],
);

return { formatTimezoneAdjustedTimestamp };
Expand Down

0 comments on commit a0a611c

Please sign in to comment.