From fe96c87a7a53f95c48ce588fdb85986b3009c5f8 Mon Sep 17 00:00:00 2001 From: amianthus <49116958+SirTenzin@users.noreply.github.com> Date: Mon, 15 Sep 2025 14:47:16 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20=F0=9F=90=9B=20timezones=20not=20localis?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../customer/analytics/AnalyticsGraph.tsx | 338 +++++++++--------- 1 file changed, 178 insertions(+), 160 deletions(-) diff --git a/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx b/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx index 8a12c2c84..f7d14afe8 100644 --- a/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx +++ b/vite/src/views/customers/customer/analytics/AnalyticsGraph.tsx @@ -1,195 +1,213 @@ +import type { AgChartOptions, FormatterParams } from "ag-charts-community"; +import { AgCharts } from "ag-charts-react"; import { AllCommunityModule, - ColDef, + type ColDef, ModuleRegistry, + type PaginationChangedEvent, + type RowDataUpdatedEvent, ValidationModule, - ValueFormatterParams, - RowDataUpdatedEvent, - PaginationChangedEvent, + type ValueFormatterParams, } from "ag-grid-community"; -import { AgChartOptions, FormatterParams } from "ag-charts-community"; -import { AgCharts } from "ag-charts-react"; // Register all Community features import { AgGridReact } from "ag-grid-react"; import { useEffect, useState } from "react"; -import { IRow, Row, autumnTheme, paginationOptions } from "./components/AGGrid"; import { useAnalyticsContext } from "./AnalyticsContext"; +import { + autumnTheme, + type IRow, + paginationOptions, + type Row, +} from "./components/AGGrid"; import { RowClickDialog } from "./components/RowClickDialog"; +const userTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + +// Helper function to parse UTC timestamps from the backend +const parseUTCTimestamp = (timestamp: string): Date => { + // If the timestamp doesn't end with 'Z' or have timezone info, assume it's UTC + if (!timestamp.includes('Z') && !timestamp.includes('+') && !timestamp.includes('-', 10)) { + // Add 'Z' to indicate UTC if it's missing + return new Date(timestamp + (timestamp.includes('T') ? 'Z' : ' UTC')); + } + return new Date(timestamp); +}; + const dateFormatter = new Intl.DateTimeFormat(navigator.language || "en-US", { - month: "short", - day: "numeric", + month: "short", + day: "numeric", + timeZone: userTimeZone, }); const hourFormatter = new Intl.DateTimeFormat(navigator.language || "en-US", { - hour: "numeric", - minute: "numeric", + hour: "numeric", + minute: "numeric", + timeZone: userTimeZone, }); const timestampFormatter = new Intl.DateTimeFormat( - navigator.language || "en-US", - { - month: "long", - day: "numeric", - hour: "numeric", - minute: "numeric", - second: "numeric", - } + navigator.language || "en-US", + { + month: "long", + day: "numeric", + hour: "numeric", + minute: "numeric", + second: "numeric", + timeZone: userTimeZone, + }, ); export function EventsBarChart({ - data, - chartConfig, + data, + chartConfig, }: { - data: { - meta: any[]; - rows: number; - data: Row[]; - }; - chartConfig: any; + data: { + meta: any[]; + rows: number; + data: Row[]; + }; + chartConfig: any; }) { - const { selectedInterval } = useAnalyticsContext(); - const [options, setOptions] = useState({ - data: data.data, - series: chartConfig, - theme: { - params: { - fontFamily: { - googleFont: "Inter", - }, - }, - palette: { - fills: [ - "#9c5aff", - "#a97eff", - "#8268ff", - "#7571ff", - "#687aff", - "#5b83ff", - "#4e8cff", - "#4195ff", - "#349eff", - "#27a7ff", - ], - }, - }, - background: { - fill: "#fafaf9", - }, - axes: [ - { - type: "category", - position: "bottom", - label: { - color: "#52525b", - }, - line: { - enabled: false, - }, - }, - { - type: "number", - position: "left", - label: { - color: "#52525b", - }, - }, - ], - formatter: { - x: (params: FormatterParams) => { - if (params.type !== "category") return; - return selectedInterval === "24h" - ? hourFormatter.format(new Date(params.value as string)) - : dateFormatter.format(new Date(params.value as string)); - }, - }, - legend: { - enabled: true, - }, - }); + const { selectedInterval } = useAnalyticsContext(); + const [options, setOptions] = useState({ + data: data.data, + series: chartConfig, + theme: { + params: { + fontFamily: { + googleFont: "Inter", + }, + }, + palette: { + fills: [ + "#9c5aff", + "#a97eff", + "#8268ff", + "#7571ff", + "#687aff", + "#5b83ff", + "#4e8cff", + "#4195ff", + "#349eff", + "#27a7ff", + ], + }, + }, + background: { + fill: "#fafaf9", + }, + axes: [ + { + type: "category", + position: "bottom", + label: { + color: "#52525b", + }, + line: { + enabled: false, + }, + }, + { + type: "number", + position: "left", + label: { + color: "#52525b", + }, + }, + ], + formatter: { + x: (params: FormatterParams) => { + if (params.type !== "category") return; + return selectedInterval === "24h" + ? hourFormatter.format(parseUTCTimestamp(params.value as string)) + : dateFormatter.format(parseUTCTimestamp(params.value as string)); + }, + }, + legend: { + enabled: true, + }, + }); - const chartData = data.data; + useEffect(() => { + setOptions((prevOptions) => ({ + ...prevOptions, + data: data.data, + series: chartConfig, + })); + }, [chartConfig, data]); - useEffect(() => { - setOptions({ - ...options, - data: data.data, - series: chartConfig, - }); - }, [chartConfig, data]); - - return ; + return ; } export function EventsAGGrid({ data }: { data: any }) { - const [rowData, setRowData] = useState([]); - const [isOpen, setIsOpen] = useState(false); - const [event, setEvent] = useState(null); - const [colDefs] = useState[]>([ - { - field: "timestamp", - flex: 1, - valueFormatter: (params: ValueFormatterParams) => { - return timestampFormatter.format(new Date(params.value as string)); - }, - cellStyle: { - paddingLeft: "2.5rem", - fontWeight: "normal", - }, - headerStyle: { - paddingLeft: "2.5rem", - }, - }, - { field: "event_name", flex: 1, cellStyle: { fontWeight: "normal" } }, - { field: "value", flex: 0, cellStyle: { fontWeight: "normal" } }, - { field: "properties", flex: 1, cellStyle: { fontWeight: "normal" } }, - ]); + const [rowData, setRowData] = useState([]); + const [isOpen, setIsOpen] = useState(false); + const [event, setEvent] = useState(null); + const [colDefs] = useState[]>([ + { + field: "timestamp", + flex: 1, + valueFormatter: (params: ValueFormatterParams) => { + return timestampFormatter.format(parseUTCTimestamp(params.value as string)); + }, + cellStyle: { + paddingLeft: "2.5rem", + fontWeight: "normal", + }, + headerStyle: { + paddingLeft: "2.5rem", + }, + }, + { field: "event_name", flex: 1, cellStyle: { fontWeight: "normal" } }, + { field: "value", flex: 0, cellStyle: { fontWeight: "normal" } }, + { field: "properties", flex: 1, cellStyle: { fontWeight: "normal" } }, + ]); - ModuleRegistry.registerModules([AllCommunityModule, ValidationModule]); + ModuleRegistry.registerModules([AllCommunityModule, ValidationModule]); - const { gridRef, pageSize, setTotalRows, setTotalPages, setCurrentPage } = - useAnalyticsContext(); + const { gridRef, pageSize, setTotalRows, setTotalPages, setCurrentPage } = + useAnalyticsContext(); - useEffect(() => { - setRowData(data.data); - }, [data]); + useEffect(() => { + setRowData(data.data); + }, [data]); - return ( -
- { - setEvent(event.data as IRow); - setIsOpen(true); - }} - onRowDataUpdated={(event: RowDataUpdatedEvent) => { - setTotalRows(event.api.paginationGetRowCount()); - }} - onPaginationChanged={(event: PaginationChangedEvent) => { - setTotalPages(event.api.paginationGetTotalPages()); - setCurrentPage(event.api.paginationGetCurrentPage() + 1); - }} - /> - {event && ( - - )} -
- ); + return ( +
+ { + setEvent(event.data as IRow); + setIsOpen(true); + }} + onRowDataUpdated={(event: RowDataUpdatedEvent) => { + setTotalRows(event.api.paginationGetRowCount()); + }} + onPaginationChanged={(event: PaginationChangedEvent) => { + setTotalPages(event.api.paginationGetTotalPages()); + setCurrentPage(event.api.paginationGetCurrentPage() + 1); + }} + /> + {event && ( + + )} +
+ ); }