Merge pull request #213 from SirTenzin/fix/event-timezone

fix: 🐛 timezones not localised
This commit is contained in:
John Yeo
2025-09-22 12:44:40 +01:00
committed by GitHub

View File

@@ -1,195 +1,213 @@
import type { AgChartOptions, FormatterParams } from "ag-charts-community";
import { AgCharts } from "ag-charts-react";
import { import {
AllCommunityModule, AllCommunityModule,
ColDef, type ColDef,
ModuleRegistry, ModuleRegistry,
type PaginationChangedEvent,
type RowDataUpdatedEvent,
ValidationModule, ValidationModule,
ValueFormatterParams, type ValueFormatterParams,
RowDataUpdatedEvent,
PaginationChangedEvent,
} from "ag-grid-community"; } from "ag-grid-community";
import { AgChartOptions, FormatterParams } from "ag-charts-community";
import { AgCharts } from "ag-charts-react";
// Register all Community features // Register all Community features
import { AgGridReact } from "ag-grid-react"; import { AgGridReact } from "ag-grid-react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { IRow, Row, autumnTheme, paginationOptions } from "./components/AGGrid";
import { useAnalyticsContext } from "./AnalyticsContext"; import { useAnalyticsContext } from "./AnalyticsContext";
import {
autumnTheme,
type IRow,
paginationOptions,
type Row,
} from "./components/AGGrid";
import { RowClickDialog } from "./components/RowClickDialog"; 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", { const dateFormatter = new Intl.DateTimeFormat(navigator.language || "en-US", {
month: "short", month: "short",
day: "numeric", day: "numeric",
timeZone: userTimeZone,
}); });
const hourFormatter = new Intl.DateTimeFormat(navigator.language || "en-US", { const hourFormatter = new Intl.DateTimeFormat(navigator.language || "en-US", {
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
timeZone: userTimeZone,
}); });
const timestampFormatter = new Intl.DateTimeFormat( const timestampFormatter = new Intl.DateTimeFormat(
navigator.language || "en-US", navigator.language || "en-US",
{ {
month: "long", month: "long",
day: "numeric", day: "numeric",
hour: "numeric", hour: "numeric",
minute: "numeric", minute: "numeric",
second: "numeric", second: "numeric",
} timeZone: userTimeZone,
},
); );
export function EventsBarChart({ export function EventsBarChart({
data, data,
chartConfig, chartConfig,
}: { }: {
data: { data: {
meta: any[]; meta: any[];
rows: number; rows: number;
data: Row[]; data: Row[];
}; };
chartConfig: any; chartConfig: any;
}) { }) {
const { selectedInterval } = useAnalyticsContext(); const { selectedInterval } = useAnalyticsContext();
const [options, setOptions] = useState<AgChartOptions>({ const [options, setOptions] = useState<AgChartOptions>({
data: data.data, data: data.data,
series: chartConfig, series: chartConfig,
theme: { theme: {
params: { params: {
fontFamily: { fontFamily: {
googleFont: "Inter", googleFont: "Inter",
}, },
}, },
palette: { palette: {
fills: [ fills: [
"#9c5aff", "#9c5aff",
"#a97eff", "#a97eff",
"#8268ff", "#8268ff",
"#7571ff", "#7571ff",
"#687aff", "#687aff",
"#5b83ff", "#5b83ff",
"#4e8cff", "#4e8cff",
"#4195ff", "#4195ff",
"#349eff", "#349eff",
"#27a7ff", "#27a7ff",
], ],
}, },
}, },
background: { background: {
fill: "#fafaf9", fill: "#fafaf9",
}, },
axes: [ axes: [
{ {
type: "category", type: "category",
position: "bottom", position: "bottom",
label: { label: {
color: "#52525b", color: "#52525b",
}, },
line: { line: {
enabled: false, enabled: false,
}, },
}, },
{ {
type: "number", type: "number",
position: "left", position: "left",
label: { label: {
color: "#52525b", color: "#52525b",
}, },
}, },
], ],
formatter: { formatter: {
x: (params: FormatterParams<any, unknown>) => { x: (params: FormatterParams<any, unknown>) => {
if (params.type !== "category") return; if (params.type !== "category") return;
return selectedInterval === "24h" return selectedInterval === "24h"
? hourFormatter.format(new Date(params.value as string)) ? hourFormatter.format(parseUTCTimestamp(params.value as string))
: dateFormatter.format(new Date(params.value as string)); : dateFormatter.format(parseUTCTimestamp(params.value as string));
}, },
}, },
legend: { legend: {
enabled: true, enabled: true,
}, },
}); });
const chartData = data.data; useEffect(() => {
setOptions((prevOptions) => ({
...prevOptions,
data: data.data,
series: chartConfig,
}));
}, [chartConfig, data]);
useEffect(() => { return <AgCharts options={options} className="h-full w-full" />;
setOptions({
...options,
data: data.data,
series: chartConfig,
});
}, [chartConfig, data]);
return <AgCharts options={options} className="h-full w-full" />;
} }
export function EventsAGGrid({ data }: { data: any }) { export function EventsAGGrid({ data }: { data: any }) {
const [rowData, setRowData] = useState<IRow[]>([]); const [rowData, setRowData] = useState<IRow[]>([]);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [event, setEvent] = useState<IRow | null>(null); const [event, setEvent] = useState<IRow | null>(null);
const [colDefs] = useState<ColDef<IRow>[]>([ const [colDefs] = useState<ColDef<IRow>[]>([
{ {
field: "timestamp", field: "timestamp",
flex: 1, flex: 1,
valueFormatter: (params: ValueFormatterParams<any, unknown>) => { valueFormatter: (params: ValueFormatterParams<any, unknown>) => {
return timestampFormatter.format(new Date(params.value as string)); return timestampFormatter.format(parseUTCTimestamp(params.value as string));
}, },
cellStyle: { cellStyle: {
paddingLeft: "2.5rem", paddingLeft: "2.5rem",
fontWeight: "normal", fontWeight: "normal",
}, },
headerStyle: { headerStyle: {
paddingLeft: "2.5rem", paddingLeft: "2.5rem",
}, },
}, },
{ field: "event_name", flex: 1, cellStyle: { fontWeight: "normal" } }, { field: "event_name", flex: 1, cellStyle: { fontWeight: "normal" } },
{ field: "value", flex: 0, cellStyle: { fontWeight: "normal" } }, { field: "value", flex: 0, cellStyle: { fontWeight: "normal" } },
{ field: "properties", flex: 1, cellStyle: { fontWeight: "normal" } }, { field: "properties", flex: 1, cellStyle: { fontWeight: "normal" } },
]); ]);
ModuleRegistry.registerModules([AllCommunityModule, ValidationModule]); ModuleRegistry.registerModules([AllCommunityModule, ValidationModule]);
const { gridRef, pageSize, setTotalRows, setTotalPages, setCurrentPage } = const { gridRef, pageSize, setTotalRows, setTotalPages, setCurrentPage } =
useAnalyticsContext(); useAnalyticsContext();
useEffect(() => { useEffect(() => {
setRowData(data.data); setRowData(data.data);
}, [data]); }, [data]);
return ( return (
<div className="w-full h-full overflow-hidden"> <div className="w-full h-full overflow-hidden">
<AgGridReact <AgGridReact
ref={gridRef} ref={gridRef}
rowData={rowData} rowData={rowData}
columnDefs={colDefs as any} columnDefs={colDefs as any}
domLayout="normal" domLayout="normal"
pagination={true} pagination={true}
paginationPageSize={pageSize} paginationPageSize={pageSize}
paginationPageSizeSelector={paginationOptions} paginationPageSizeSelector={paginationOptions}
suppressPaginationPanel={true} suppressPaginationPanel={true}
className="w-full h-full" className="w-full h-full"
theme={autumnTheme} theme={autumnTheme}
defaultColDef={{ defaultColDef={{
flex: 1, flex: 1,
resizable: true, resizable: true,
sortable: true, sortable: true,
filter: true, filter: true,
}} }}
onRowClicked={(event) => { onRowClicked={(event) => {
setEvent(event.data as IRow); setEvent(event.data as IRow);
setIsOpen(true); setIsOpen(true);
}} }}
onRowDataUpdated={(event: RowDataUpdatedEvent) => { onRowDataUpdated={(event: RowDataUpdatedEvent) => {
setTotalRows(event.api.paginationGetRowCount()); setTotalRows(event.api.paginationGetRowCount());
}} }}
onPaginationChanged={(event: PaginationChangedEvent) => { onPaginationChanged={(event: PaginationChangedEvent) => {
setTotalPages(event.api.paginationGetTotalPages()); setTotalPages(event.api.paginationGetTotalPages());
setCurrentPage(event.api.paginationGetCurrentPage() + 1); setCurrentPage(event.api.paginationGetCurrentPage() + 1);
}} }}
/> />
{event && ( {event && (
<RowClickDialog event={event} isOpen={isOpen} setIsOpen={setIsOpen} /> <RowClickDialog event={event} isOpen={isOpen} setIsOpen={setIsOpen} />
)} )}
</div> </div>
); );
} }