import React, { useCallback, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { DateTime } from "luxon"; import { FilterPreset, Granularity, isGranularity } from "../types/dashboard"; import { useRouter } from "./RouterContext"; export interface DashboardFilter { startDate: DateTime; endDate: DateTime; granularity: Granularity; preset?: FilterPreset; } export interface DashboardFilterContext { filter: DashboardFilter; setFilter: (filter: DashboardFilter) => void; } /** * The default filter for the dashboard,used when the filter is not present in the URL. * Might as well be a function to avoid stale dates. */ export function defaultFilter(): DashboardFilter { return dashboardFilterFromPreset("last_7_days"); } export function dashboardFilterFromPreset(preset: FilterPreset): DashboardFilter { switch (preset) { case "last_7_days": return { preset: "last_7_days", granularity: "day", startDate: DateTime.now().minus({ days: 6 }).startOf("day"), endDate: DateTime.now().endOf("day"), }; case "last_30_days": return { preset: "last_30_days", granularity: "day", startDate: DateTime.now().minus({ days: 29 }).startOf("day"), endDate: DateTime.now().endOf("day"), }; case "today": return { preset: "today", granularity: "day", startDate: DateTime.now().startOf("day"), endDate: DateTime.now().endOf("day"), }; case "yesterday": return { preset: "yesterday", granularity: "day", startDate: DateTime.now().minus({ days: 1 }).startOf("day"), endDate: DateTime.now().minus({ days: 1 }).endOf("day"), }; case "this_week": return { preset: "this_week", granularity: "day", startDate: DateTime.now().startOf("week"), endDate: DateTime.now().endOf("day"), }; case "last_week": return { preset: "last_week", granularity: "day", startDate: DateTime.now().minus({ weeks: 1 }).startOf("week"), endDate: DateTime.now().minus({ weeks: 1 }).endOf("week"), }; case "this_month": return { preset: "this_month", granularity: "day", startDate: DateTime.now().startOf("month"), endDate: DateTime.now().endOf("day"), }; case "last_month": return { preset: "last_month", granularity: "day", startDate: DateTime.now().minus({ months: 1 }).startOf("month"), endDate: DateTime.now().minus({ months: 1 }).endOf("month"), }; case "this_year": return { preset: "this_year", granularity: "month", startDate: DateTime.now().startOf("year"), endDate: DateTime.now().endOf("day"), }; case "last_year": return { preset: "last_year", granularity: "month", startDate: DateTime.now().minus({ years: 1 }).startOf("year"), endDate: DateTime.now().minus({ years: 1 }).endOf("year"), }; } throw new TypeError(`Unknown filter preset: ${preset}`); } export const filterDateRangeLabel = (filter: DashboardFilter) => { const { startDate, endDate } = filter; const isSameDay = startDate.valueOf() == endDate.startOf("day").valueOf(); const startFormat = startDate.year === endDate.year ? "dd LLL" : "dd LLL, yyyy"; return isSameDay ? startDate.toFormat("dd LLL, yyyy") : `${startDate.toFormat(startFormat)} - ${endDate.toFormat("dd LLL, yyyy")}`; }; export const presetDateRangeLabel = (preset: FilterPreset) => { return filterDateRangeLabel(dashboardFilterFromPreset(preset)); }; /** * Load the initial filter from session storage. * If the filter is not present or invalid, return `defaultFilter`. */ export function dashboardFilterFromSearchParams(searchParams: URLSearchParams): DashboardFilter { // The URL always has to store the inclusive date since the backend expects it. const searchEndDate = searchParams.get("end_date"); const searchStartDate = searchParams.get("start_date"); const granularity = searchParams.get("granularity"); if (searchStartDate == null || searchEndDate == null) return defaultFilter(); if (!isGranularity(granularity)) return defaultFilter(); const startDate = DateTime.fromISO(searchStartDate); const endDate = DateTime.fromISO(searchEndDate).minus({ days: 1 }).endOf("day"); return { startDate, endDate, granularity, preset: "custom", // TODO: Detect current preset, e.g. preset query param }; } export const DashboardFilterContext = React.createContext( undefined as unknown as DashboardFilterContext, ); export const useDashboardFilter = () => React.useContext(DashboardFilterContext); export interface DashboardFilterContextProviderProps { children: React.ReactNode; } /** * Convert `DashboardFilter` to a query string that can be used in the URL. * Uses the inclusive `end_date` to make sure the backend always get the correct date. */ export function dashboardFilterToSearchParams({ startDate, endDate, granularity, }: DashboardFilter): Record | null { const start_date = startDate.toISO({ suppressSeconds: true, suppressMilliseconds: true }); const end_date = endDate .plus({ days: 1 }) .startOf("day") .toISO({ suppressSeconds: true, suppressMilliseconds: true }); if (start_date == null || end_date == null) return null; return { start_date, end_date, granularity }; } const DashboardFilterContextProvider: React.FC = ({ children, }) => { const { navigate } = useRouter(); const [searchParams] = useSearchParams(); const [filter, setFilterState] = useState( dashboardFilterFromSearchParams(searchParams), ); const setFilter = useCallback((newFilter: DashboardFilter) => { setFilterState(newFilter); const query = dashboardFilterToSearchParams(newFilter); if (query == null) { console.error("[DASHBOARD_FILTER_CONTEXT] filter", newFilter); return; } navigate(undefined, { query, replace: true }); }, []); return ( {children} ); }; export default DashboardFilterContextProvider;