diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index eb024e37..07601ff6 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -78,7 +78,7 @@ "nextjs-toploader": "^1.6.11", "nuqs": "^1.16.1", "prisma-error-enum": "^0.1.3", - "pushmodal": "^1.0.0", + "pushmodal": "^1.0.3", "ramda": "^0.29.1", "random-animal-name": "^0.1.1", "react": "18.2.0", diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx index 852d3409..200fdcfc 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx @@ -22,6 +22,7 @@ import { toast } from 'sonner'; import { getDefaultIntervalByDates, getDefaultIntervalByRange, + timeWindows, } from '@openpanel/constants'; import type { getReportsByDashboardId } from '@openpanel/db'; @@ -64,7 +65,7 @@ export function ListReports({ reports }: ListReportsProps) {
{reports.map((report) => { - const chartRange = report.range; // timeRanges[report.range]; + const chartRange = report.range; return (
- {chartRange} + {timeWindows[chartRange].label} {startDate && endDate ? ( Custom dates diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts/events-per-day-chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts/events-per-day-chart.tsx index f4a0d8a2..763cfff8 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts/events-per-day-chart.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/charts/events-per-day-chart.tsx @@ -23,7 +23,7 @@ export function EventsPerDayChart({ projectId, filters, events }: Props) {
0 diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/overview-sticky-header.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/overview-sticky-header.tsx index e95831bf..dbc335d2 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/overview-sticky-header.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/overview-sticky-header.tsx @@ -1,37 +1,20 @@ 'use client'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; -import { ReportRange } from '@/components/report/ReportRange'; -import { endOfDay, startOfDay } from 'date-fns'; +import { TimeWindowPicker } from '@/components/time-window-picker'; export function OverviewReportRange() { - const { range, setRange, setEndDate, setStartDate, startDate, endDate } = + const { range, setRange, setStartDate, setEndDate, endDate, startDate } = useOverviewOptions(); - return ( - { - setRange(value); - setStartDate(null); - setEndDate(null); - }} - dates={{ - startDate, - endDate, - }} - onDatesChange={(val) => { - if (!val) return; - if (val.from && val.to) { - setRange(null); - setStartDate(startOfDay(val.from).toISOString()); - setEndDate(endOfDay(val.to).toISOString()); - } else if (val.from) { - setStartDate(startOfDay(val.from).toISOString()); - } else if (val.to) { - setEndDate(endOfDay(val.to).toISOString()); - } - }} + return ( + setStartDate(date)} + onEndDateChange={(date) => setEndDate(date)} + endDate={endDate} + startDate={startDate} /> ); } diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx index a30223fd..503876f4 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx @@ -114,7 +114,7 @@ export default async function Page({ lineType: 'monotone', interval: 'day', name: 'Events', - range: '1m', + range: '30d', previous: false, metric: 'sum', }; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/report-editor.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/report-editor.tsx index eaea10ad..6aac0531 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/report-editor.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/report-editor.tsx @@ -6,11 +6,9 @@ import { ChartSwitch } from '@/components/report/chart'; import { ReportChartType } from '@/components/report/ReportChartType'; import { ReportInterval } from '@/components/report/ReportInterval'; import { ReportLineType } from '@/components/report/ReportLineType'; -import { ReportRange } from '@/components/report/ReportRange'; import { ReportSaveButton } from '@/components/report/ReportSaveButton'; import { changeDateRanges, - changeDates, changeEndDate, changeStartDate, ready, @@ -18,6 +16,7 @@ import { setReport, } from '@/components/report/reportSlice'; import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar'; +import { TimeWindowPicker } from '@/components/time-window-picker'; import { Button } from '@/components/ui/button'; import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'; import { useAppParams } from '@/hooks/useAppParams'; @@ -63,32 +62,20 @@ export default function ReportEditor({
- { + onChange={(value) => { dispatch(changeDateRanges(value)); }} - dates={{ - startDate: report.startDate, - endDate: report.endDate, - }} - onDatesChange={(val) => { - if (!val) return; - - if (val.from && val.to) { - dispatch( - changeDates({ - startDate: startOfDay(val.from).toISOString(), - endDate: endOfDay(val.to).toISOString(), - }) - ); - } else if (val.from) { - dispatch(changeStartDate(startOfDay(val.from).toISOString())); - } else if (val.to) { - dispatch(changeEndDate(endOfDay(val.to).toISOString())); - } - }} + value={report.range} + onStartDateChange={(date) => + dispatch(changeStartDate(startOfDay(date).toISOString())) + } + onEndDateChange={(date) => + dispatch(changeEndDate(endOfDay(date).toISOString())) + } + endDate={report.endDate} + startDate={report.startDate} /> diff --git a/apps/dashboard/src/components/overview/useOverviewOptions.ts b/apps/dashboard/src/components/overview/useOverviewOptions.ts index 613e96b5..c0293243 100644 --- a/apps/dashboard/src/components/overview/useOverviewOptions.ts +++ b/apps/dashboard/src/components/overview/useOverviewOptions.ts @@ -1,3 +1,4 @@ +import { useEffect } from 'react'; import { parseAsBoolean, parseAsInteger, @@ -9,8 +10,9 @@ import { import { getDefaultIntervalByDates, getDefaultIntervalByRange, - timeRanges, + timeWindows, } from '@openpanel/constants'; +import type { IChartRange } from '@openpanel/validation'; import { mapKeys } from '@openpanel/validation'; const nuqsOptions = { history: 'push' } as const; @@ -30,7 +32,7 @@ export function useOverviewOptions() { ); const [range, setRange] = useQueryState( 'range', - parseAsStringEnum(mapKeys(timeRanges)) + parseAsStringEnum(mapKeys(timeWindows)) .withDefault('7d') .withOptions(nuqsOptions) ); @@ -54,7 +56,13 @@ export function useOverviewOptions() { previous, setPrevious, range, - setRange, + setRange: (value: IChartRange | null) => { + if (value !== 'custom') { + setStartDate(null); + setEndDate(null); + } + setRange(value); + }, metric, setMetric, startDate, diff --git a/apps/dashboard/src/components/report/ReportInterval.tsx b/apps/dashboard/src/components/report/ReportInterval.tsx index 087ede36..453ad2ca 100644 --- a/apps/dashboard/src/components/report/ReportInterval.tsx +++ b/apps/dashboard/src/components/report/ReportInterval.tsx @@ -5,7 +5,6 @@ import { isHourIntervalEnabledByRange, isMinuteIntervalEnabledByRange, } from '@openpanel/constants'; -import type { IInterval } from '@openpanel/validation'; import { Combobox } from '../ui/combobox'; import { changeInterval } from './reportSlice'; @@ -55,10 +54,7 @@ export function ReportInterval({ className }: ReportIntervalProps) { value: 'month', label: 'Month', disabled: - range === 'today' || - range === '24h' || - range === '1h' || - range === '30min', + range === 'today' || range === 'lastHour' || range === '30min', }, ]} /> diff --git a/apps/dashboard/src/components/report/ReportRange.tsx b/apps/dashboard/src/components/report/ReportRange.tsx deleted file mode 100644 index d2a82d6d..00000000 --- a/apps/dashboard/src/components/report/ReportRange.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import * as React from 'react'; -import { Button } from '@/components/ui/button'; -import { Calendar } from '@/components/ui/calendar'; -import { - Popover, - PopoverContent, - PopoverTrigger, -} from '@/components/ui/popover'; -import { useBreakpoint } from '@/hooks/useBreakpoint'; -import { cn } from '@/utils/cn'; -import { format } from 'date-fns'; -import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react'; -import type { SelectRangeEventHandler } from 'react-day-picker'; - -import { timeRanges } from '@openpanel/constants'; -import type { IChartRange } from '@openpanel/validation'; - -import type { ExtendedComboboxProps } from '../ui/combobox'; -import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group'; - -export function ReportRange({ - range, - onRangeChange, - onDatesChange, - dates, - className, - ...props -}: { - range: IChartRange; - onRangeChange: (range: IChartRange) => void; - onDatesChange: SelectRangeEventHandler; - dates: { startDate: string | null; endDate: string | null }; -} & Omit, 'value' | 'onChange'>) { - const { isBelowSm } = useBreakpoint('sm'); - - return ( - <> - - - - - -
- { - if (value) onRangeChange(value as IChartRange); - }} - type="single" - variant="outline" - className="flex-wrap max-sm:max-w-xs" - > - {Object.values(timeRanges).map((key) => ( - - {key} - - ))} - -
- -
-
- - ); -} diff --git a/apps/dashboard/src/components/report/reportSlice.ts b/apps/dashboard/src/components/report/reportSlice.ts index addef9f8..0616a331 100644 --- a/apps/dashboard/src/components/report/reportSlice.ts +++ b/apps/dashboard/src/components/report/reportSlice.ts @@ -1,4 +1,3 @@ -import { start } from 'repl'; import { createSlice } from '@reduxjs/toolkit'; import type { PayloadAction } from '@reduxjs/toolkit'; import { isSameDay, isSameMonth } from 'date-fns'; @@ -39,7 +38,7 @@ const initialState: InitialState = { interval: 'day', breakdowns: [], events: [], - range: '1m', + range: '30d', startDate: null, endDate: null, previous: false, @@ -232,10 +231,11 @@ export const reportSlice = createSlice({ changeDateRanges: (state, action: PayloadAction) => { state.dirty = true; state.range = action.payload; - state.startDate = null; - state.endDate = null; - - state.interval = getDefaultIntervalByRange(action.payload); + if (action.payload !== 'custom') { + state.startDate = null; + state.endDate = null; + state.interval = getDefaultIntervalByRange(action.payload); + } }, // Formula diff --git a/apps/dashboard/src/components/time-window-picker.tsx b/apps/dashboard/src/components/time-window-picker.tsx new file mode 100644 index 00000000..d4cab455 --- /dev/null +++ b/apps/dashboard/src/components/time-window-picker.tsx @@ -0,0 +1,196 @@ +import { useCallback, useEffect, useRef } from 'react'; +import { Button } from '@/components/ui/button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { pushModal, useOnPushModal } from '@/modals'; +import { cn } from '@/utils/cn'; +import { bind } from 'bind-event-listener'; +import { CalendarIcon } from 'lucide-react'; + +import { timeWindows } from '@openpanel/constants'; +import type { IChartRange } from '@openpanel/validation'; + +function shouldIgnoreKeypress(event: KeyboardEvent) { + const tagName = (event?.target as HTMLElement)?.tagName; + const modifierPressed = + event.ctrlKey || event.metaKey || event.altKey || event.keyCode == 229; + const isTyping = + event.isComposing || tagName == 'INPUT' || tagName == 'TEXTAREA'; + + return modifierPressed || isTyping; +} + +type Props = { + value: IChartRange; + onChange: (value: IChartRange) => void; + onStartDateChange: (date: string) => void; + onEndDateChange: (date: string) => void; + endDate: string | null; + startDate: string | null; + className?: string; +}; +export function TimeWindowPicker({ + value, + onChange, + startDate, + onStartDateChange, + endDate, + onEndDateChange, + className, +}: Props) { + const isDateRangerPickerOpen = useRef(false); + useOnPushModal( + 'DateRangerPicker', + (open) => (isDateRangerPickerOpen.current = open) + ); + const timeWindow = timeWindows[value ?? '30d']; + + const handleCustom = useCallback(() => { + pushModal('DateRangerPicker', { + onChange: ({ startDate, endDate }) => { + onStartDateChange(startDate.toISOString()); + onEndDateChange(endDate.toISOString()); + onChange('custom'); + }, + startDate: startDate ? new Date(startDate) : undefined, + endDate: endDate ? new Date(endDate) : undefined, + }); + }, [startDate, endDate]); + + useEffect(() => { + return bind(document, { + type: 'keydown', + listener(event) { + if (shouldIgnoreKeypress(event)) { + return; + } + + if (isDateRangerPickerOpen.current) { + return; + } + + const match = Object.values(timeWindows).find( + (tw) => event.key === tw.shortcut.toLowerCase() + ); + if (match?.key === 'custom') { + handleCustom(); + } else if (match) { + onChange(match.key); + } + }, + }); + }, [handleCustom]); + + return ( + + + + + + Time window + + + + onChange(timeWindows['30min'].key)}> + {timeWindows['30min'].label} + + {timeWindows['30min'].shortcut} + + + onChange(timeWindows.lastHour.key)}> + {timeWindows.lastHour.label} + + {timeWindows.lastHour.shortcut} + + + onChange(timeWindows.today.key)}> + {timeWindows.today.label} + + {timeWindows.today.shortcut} + + + + + + + + onChange(timeWindows['7d'].key)}> + {timeWindows['7d'].label} + + {timeWindows['7d'].shortcut} + + + onChange(timeWindows['30d'].key)}> + {timeWindows['30d'].label} + + {timeWindows['30d'].shortcut} + + + + + + + + onChange(timeWindows.monthToDate.key)} + > + {timeWindows.monthToDate.label} + + {timeWindows.monthToDate.shortcut} + + + onChange(timeWindows.lastMonth.key)}> + {timeWindows.lastMonth.label} + + {timeWindows.lastMonth.shortcut} + + + + + + + + onChange(timeWindows.yearToDate.key)} + > + {timeWindows.yearToDate.label} + + {timeWindows.yearToDate.shortcut} + + + onChange(timeWindows.lastYear.key)}> + {timeWindows.lastYear.label} + + {timeWindows.lastYear.shortcut} + + + + + + + + handleCustom()}> + {timeWindows.custom.label} + + {timeWindows.custom.shortcut} + + + + + + ); +} diff --git a/apps/dashboard/src/components/ui/button.tsx b/apps/dashboard/src/components/ui/button.tsx index 103f4a95..9c54cb6f 100644 --- a/apps/dashboard/src/components/ui/button.tsx +++ b/apps/dashboard/src/components/ui/button.tsx @@ -10,7 +10,7 @@ import { Loader2 } from 'lucide-react'; import Link from 'next/link'; const buttonVariants = cva( - 'inline-flex flex-shrink-0 items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + 'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', { variants: { variant: { diff --git a/apps/dashboard/src/modals/DateRangerPicker.tsx b/apps/dashboard/src/modals/DateRangerPicker.tsx new file mode 100644 index 00000000..219162e5 --- /dev/null +++ b/apps/dashboard/src/modals/DateRangerPicker.tsx @@ -0,0 +1,65 @@ +import { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { Calendar } from '@/components/ui/calendar'; +import { useBreakpoint } from '@/hooks/useBreakpoint'; +import { subMonths } from 'date-fns'; + +import { popModal } from '.'; +import { ModalContent, ModalHeader } from './Modal/Container'; + +type Props = { + onChange: (payload: { startDate: Date; endDate: Date }) => void; + startDate?: Date; + endDate?: Date; +}; +export default function DateRangerPicker({ + onChange, + startDate: initialStartDate, + endDate: initialEndDate, +}: Props) { + const { isBelowSm } = useBreakpoint('sm'); + const [startDate, setStartDate] = useState(initialStartDate); + const [endDate, setEndDate] = useState(initialEndDate); + return ( + + + { + if (range?.from) { + setStartDate(range.from); + } + if (range?.to) { + setEndDate(range.to); + } + }} + numberOfMonths={isBelowSm ? 1 : 2} + className="min-h-[350px] [&_table]:mx-auto [&_table]:w-auto" + /> + + + ); +} diff --git a/apps/dashboard/src/modals/Modal/Container.tsx b/apps/dashboard/src/modals/Modal/Container.tsx index ff1e42e9..01979763 100644 --- a/apps/dashboard/src/modals/Modal/Container.tsx +++ b/apps/dashboard/src/modals/Modal/Container.tsx @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'; import { DialogContent } from '@/components/ui/dialog'; +import { cn } from '@/utils/cn'; import type { DialogContentProps } from '@radix-ui/react-dialog'; import { X } from 'lucide-react'; @@ -19,11 +20,17 @@ interface ModalHeaderProps { title: string | React.ReactNode; text?: string | React.ReactNode; onClose?: (() => void) | false; + className?: string; } -export function ModalHeader({ title, text, onClose }: ModalHeaderProps) { +export function ModalHeader({ + title, + text, + onClose, + className, +}: ModalHeaderProps) { return ( -
+
{title}
{!!text &&
{text}
} diff --git a/apps/dashboard/src/modals/index.tsx b/apps/dashboard/src/modals/index.tsx index 2442ed4b..4959fc6a 100644 --- a/apps/dashboard/src/modals/index.tsx +++ b/apps/dashboard/src/modals/index.tsx @@ -59,11 +59,19 @@ const modals = { FunnelStepDetails: dynamic(() => import('./FunnelStepDetails'), { loading: Loading, }), + DateRangerPicker: dynamic(() => import('./DateRangerPicker'), { + loading: Loading, + }), }; -export const { pushModal, popModal, popAllModals, ModalProvider } = - createPushModal({ - modals, - }); +export const { + pushModal, + popModal, + popAllModals, + ModalProvider, + useOnPushModal, +} = createPushModal({ + modals, +}); export const showConfirm = (props: ConfirmProps) => pushModal('Confirm', props); diff --git a/packages/constants/index.ts b/packages/constants/index.ts index edc99d10..c2bf653f 100644 --- a/packages/constants/index.ts +++ b/packages/constants/index.ts @@ -2,6 +2,59 @@ import { isSameDay, isSameMonth } from 'date-fns'; export const NOT_SET_VALUE = '(not set)'; +export const timeWindows = { + '30min': { + key: '30min', + label: 'Last 30 min', + shortcut: 'R', + }, + lastHour: { + key: 'lastHour', + label: 'Last hour', + shortcut: 'H', + }, + today: { + key: 'today', + label: 'Today', + shortcut: 'D', + }, + '7d': { + key: '7d', + label: 'Last 7 days', + shortcut: 'W', + }, + '30d': { + key: '30d', + label: 'Last 30 days', + shortcut: 'T', + }, + monthToDate: { + key: 'monthToDate', + label: 'Month to Date', + shortcut: 'M', + }, + lastMonth: { + key: 'lastMonth', + label: 'Last Month', + shortcut: 'P', + }, + yearToDate: { + key: 'yearToDate', + label: 'Year to Date', + shortcut: 'Y', + }, + lastYear: { + key: 'lastYear', + label: 'Last year', + shortcut: 'U', + }, + custom: { + key: 'custom', + label: 'Custom range', + shortcut: 'C', + }, +} as const; + export const ProjectTypeNames = { website: 'Website', app: 'App', @@ -64,7 +117,7 @@ export const alphabetIds = [ 'J', ] as const; -export const timeRanges = { +export const deprecated_timeRanges = { '30min': '30min', '1h': '1h', today: 'today', @@ -84,26 +137,29 @@ export const metrics = { max: 'max', } as const; -export function isMinuteIntervalEnabledByRange(range: keyof typeof timeRanges) { - return range === '30min' || range === '1h'; +export function isMinuteIntervalEnabledByRange( + range: keyof typeof timeWindows +) { + return range === '30min' || range === 'lastHour'; } -export function isHourIntervalEnabledByRange(range: keyof typeof timeRanges) { - return ( - isMinuteIntervalEnabledByRange(range) || - range === 'today' || - range === '24h' - ); +export function isHourIntervalEnabledByRange(range: keyof typeof timeWindows) { + return isMinuteIntervalEnabledByRange(range) || range === 'today'; } export function getDefaultIntervalByRange( - range: keyof typeof timeRanges + range: keyof typeof timeWindows ): keyof typeof intervals { - if (range === '30min' || range === '1h') { + if (range === '30min' || range === 'lastHour') { return 'minute'; - } else if (range === 'today' || range === '24h') { + } else if (range === 'today') { return 'hour'; - } else if (range === '7d' || range === '14d' || range === '1m') { + } else if ( + range === '7d' || + range === '30d' || + range === 'lastMonth' || + range === 'monthToDate' + ) { return 'day'; } return 'month'; diff --git a/packages/db/prisma/migrations/20240427075544_change_default/migration.sql b/packages/db/prisma/migrations/20240427075544_change_default/migration.sql new file mode 100644 index 00000000..34c71d15 --- /dev/null +++ b/packages/db/prisma/migrations/20240427075544_change_default/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "reports" ALTER COLUMN "range" SET DEFAULT '30d'; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 1441b4ea..6cd60d43 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -163,7 +163,7 @@ model Report { id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid name String interval Interval - range String @default("1m") + range String @default("30d") chartType ChartType lineType String @default("monotone") breakdowns Json diff --git a/packages/db/src/services/reports.service.ts b/packages/db/src/services/reports.service.ts index b33fb950..75b51260 100644 --- a/packages/db/src/services/reports.service.ts +++ b/packages/db/src/services/reports.service.ts @@ -1,4 +1,8 @@ -import { alphabetIds, lineTypes, timeRanges } from '@openpanel/constants'; +import { + alphabetIds, + deprecated_timeRanges, + lineTypes, +} from '@openpanel/constants'; import type { IChartBreakdown, IChartEvent, @@ -52,7 +56,10 @@ export function transformReport( lineType: (report.lineType as IChartLineType) ?? lineTypes.monotone, interval: report.interval, name: report.name || 'Untitled', - range: (report.range as IChartRange) ?? timeRanges['1m'], + range: + report.range in deprecated_timeRanges + ? '30d' + : (report.range as IChartRange), previous: report.previous ?? false, formula: report.formula ?? undefined, metric: report.metric ?? 'sum', diff --git a/packages/trpc/src/routers/chart.helpers.ts b/packages/trpc/src/routers/chart.helpers.ts index 08adeb1e..44004c24 100644 --- a/packages/trpc/src/routers/chart.helpers.ts +++ b/packages/trpc/src/routers/chart.helpers.ts @@ -1,4 +1,14 @@ -import { subDays } from 'date-fns'; +import { + endOfDay, + endOfYear, + startOfDay, + startOfMonth, + startOfYear, + subDays, + subMinutes, + subMonths, + subYears, +} from 'date-fns'; import * as mathjs from 'mathjs'; import { repeat, reverse, sort } from 'ramda'; import { escape } from 'sqlstring'; @@ -298,22 +308,9 @@ export async function getChartData(payload: IGetChartDataInput) { } export function getDatesFromRange(range: IChartRange) { - if (range === 'today') { - const startDate = new Date(); - const endDate = new Date(); - startDate.setUTCHours(0, 0, 0, 0); - endDate.setUTCHours(23, 59, 59, 999); - - return { - startDate: startDate.toUTCString(), - endDate: endDate.toUTCString(), - }; - } - - if (range === '30min' || range === '1h') { - const startDate = new Date( - Date.now() - 1000 * 60 * (range === '30min' ? 30 : 60) - ).toUTCString(); + if (range === '30min' || range === 'lastHour') { + const minutes = range === '30min' ? 30 : 60; + const startDate = subMinutes(new Date(), minutes).toUTCString(); const endDate = new Date().toUTCString(); return { @@ -322,36 +319,92 @@ export function getDatesFromRange(range: IChartRange) { }; } - let days = 1; + if (range === 'today') { + const startDate = startOfDay(new Date()); + const endDate = endOfDay(new Date()); - if (range === '24h') { - const startDate = subDays(new Date(), days); - const endDate = new Date(); return { startDate: startDate.toUTCString(), endDate: endDate.toUTCString(), }; - } else if (range === '7d') { - days = 7; - } else if (range === '14d') { - days = 14; - } else if (range === '1m') { - days = 30; - } else if (range === '3m') { - days = 90; - } else if (range === '6m') { - days = 180; - } else if (range === '1y') { - days = 365; } - const startDate = subDays(new Date(), days); - startDate.setUTCHours(0, 0, 0, 0); - const endDate = new Date(); - endDate.setUTCHours(23, 59, 59, 999); + if (range === '7d') { + const startDate = subDays(new Date(), 7).toUTCString(); + const endDate = new Date().toUTCString(); + + return { + startDate, + endDate, + }; + } + + if (range === '30d') { + const startDate = subDays(new Date(), 30).toUTCString(); + const endDate = new Date().toUTCString(); + + return { + startDate, + endDate, + }; + } + + if (range === 'monthToDate') { + const startDate = startOfMonth(new Date()).toUTCString(); + const endDate = new Date().toUTCString(); + + return { + startDate, + endDate, + }; + } + + if (range === 'lastMonth') { + const month = subMonths(new Date(), 1); + const startDate = startOfMonth(month).toUTCString(); + const endDate = endOfDay(month).toUTCString(); + + return { + startDate, + endDate, + }; + } + + if (range === 'yearToDate') { + const startDate = startOfYear(new Date()).toUTCString(); + const endDate = new Date().toUTCString(); + + return { + startDate, + endDate, + }; + } + + if (range === 'lastYear') { + const year = subYears(new Date(), 1); + const startDate = startOfYear(year).toUTCString(); + const endDate = endOfYear(year).toUTCString(); + + return { + startDate, + endDate, + }; + } + + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); + console.log('-------------------------------'); return { - startDate: startDate.toUTCString(), - endDate: endDate.toUTCString(), + startDate: subDays(new Date(), 30).toISOString(), + endDate: new Date().toISOString(), }; } @@ -374,44 +427,7 @@ export function getChartPrevStartEndDate({ endDate: string; range: IChartRange; }) { - let diff = 0; - - switch (range) { - case '30min': { - diff = 1000 * 60 * 30; - break; - } - case '1h': { - diff = 1000 * 60 * 60; - break; - } - case '24h': - case 'today': { - diff = 1000 * 60 * 60 * 24; - break; - } - case '7d': { - diff = 1000 * 60 * 60 * 24 * 7; - break; - } - case '14d': { - diff = 1000 * 60 * 60 * 24 * 14; - break; - } - case '1m': { - diff = 1000 * 60 * 60 * 24 * 30; - break; - } - case '3m': { - diff = 1000 * 60 * 60 * 24 * 90; - break; - } - case '6m': { - diff = 1000 * 60 * 60 * 24 * 180; - break; - } - } - + const diff = new Date(endDate).getTime() - new Date(startDate).getTime(); return { startDate: new Date(new Date(startDate).getTime() - diff).toISOString(), endDate: new Date(new Date(endDate).getTime() - diff).toISOString(), diff --git a/packages/trpc/src/routers/report.ts b/packages/trpc/src/routers/report.ts index 2f7ae0a8..b6eb8743 100644 --- a/packages/trpc/src/routers/report.ts +++ b/packages/trpc/src/routers/report.ts @@ -29,7 +29,7 @@ export const reportRouter = createTRPCRouter({ breakdowns: report.breakdowns, chartType: report.chartType, lineType: report.lineType, - range: report.range, + range: report.range === 'custom' ? '30d' : report.range, formula: report.formula, }, }); @@ -53,7 +53,7 @@ export const reportRouter = createTRPCRouter({ breakdowns: report.breakdowns, chartType: report.chartType, lineType: report.lineType, - range: report.range, + range: report.range === 'custom' ? '30d' : report.range, formula: report.formula, }, }); diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index fe18450b..0f49491a 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -6,7 +6,7 @@ import { lineTypes, metrics, operators, - timeRanges, + timeWindows, } from '@openpanel/constants'; export function objectToZodEnums( @@ -57,7 +57,7 @@ export const zTimeInterval = z.enum(objectToZodEnums(intervals)); export const zMetric = z.enum(objectToZodEnums(metrics)); -export const zRange = z.enum(objectToZodEnums(timeRanges)); +export const zRange = z.enum(objectToZodEnums(timeWindows)); export const zChartInput = z.object({ name: z.string().default(''), @@ -66,7 +66,7 @@ export const zChartInput = z.object({ interval: zTimeInterval.default('day'), events: zChartEvents, breakdowns: zChartBreakdowns.default([]), - range: zRange.default('1m'), + range: zRange.default('30d'), previous: z.boolean().default(false), formula: z.string().optional(), metric: zMetric.default('sum'), diff --git a/packages/validation/src/types.validation.ts b/packages/validation/src/types.validation.ts index 73cc2f72..89a4e17f 100644 --- a/packages/validation/src/types.validation.ts +++ b/packages/validation/src/types.validation.ts @@ -1,7 +1,5 @@ import type { z } from 'zod'; -import type { timeRanges } from '@openpanel/constants'; - import type { zChartBreakdown, zChartEvent, @@ -9,6 +7,7 @@ import type { zChartType, zLineType, zMetric, + zRange, zTimeInterval, } from './index'; @@ -24,7 +23,7 @@ export type IInterval = z.infer; export type IChartType = z.infer; export type IChartMetric = z.infer; export type IChartLineType = z.infer; -export type IChartRange = keyof typeof timeRanges; +export type IChartRange = z.infer; export type IGetChartDataInput = { event: IChartEvent; projectId: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6103305e..d35fcfaa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -334,8 +334,8 @@ importers: specifier: ^0.1.3 version: 0.1.3 pushmodal: - specifier: ^1.0.0 - version: 1.0.0(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0) + specifier: ^1.0.3 + version: 1.0.3(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0) ramda: specifier: ^0.29.1 version: 0.29.1 @@ -10085,7 +10085,7 @@ packages: eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.29.1)(eslint@8.56.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.56.0) eslint-plugin-jsx-a11y: 6.8.0(eslint@8.56.0) eslint-plugin-react: 7.33.2(eslint@8.56.0) eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0) @@ -10166,7 +10166,7 @@ packages: dependencies: debug: 4.3.4 eslint: 8.56.0 - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.56.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.56.0) glob: 7.2.3 is-glob: 4.0.3 resolve: 1.22.8 @@ -10198,36 +10198,6 @@ packages: - supports-color dev: false - /eslint-module-utils@2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1)(eslint@8.56.0): - resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} - engines: {node: '>=4'} - peerDependencies: - '@typescript-eslint/parser': '*' - eslint: '*' - eslint-import-resolver-node: '*' - eslint-import-resolver-typescript: '*' - eslint-import-resolver-webpack: '*' - peerDependenciesMeta: - '@typescript-eslint/parser': - optional: true - eslint: - optional: true - eslint-import-resolver-node: - optional: true - eslint-import-resolver-typescript: - optional: true - eslint-import-resolver-webpack: - optional: true - dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) - debug: 3.2.7 - eslint: 8.56.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 2.7.1(eslint-plugin-import@2.29.1)(eslint@8.56.0) - transitivePeerDependencies: - - supports-color - dev: false - /eslint-module-utils@2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==} engines: {node: '>=4'} @@ -10258,7 +10228,7 @@ packages: - supports-color dev: false - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-typescript@2.7.1)(eslint@8.56.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -10268,7 +10238,7 @@ packages: '@typescript-eslint/parser': optional: true dependencies: - '@typescript-eslint/parser': 5.62.0(eslint@8.56.0)(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.3.3) array-includes: 3.1.7 array.prototype.findlastindex: 1.2.4 array.prototype.flat: 1.3.2 @@ -10277,7 +10247,7 @@ packages: doctrine: 2.1.0 eslint: 8.56.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.8.0(@typescript-eslint/parser@5.62.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@2.7.1)(eslint@8.56.0) + eslint-module-utils: 2.8.0(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0) hasown: 2.0.1 is-core-module: 2.13.1 is-glob: 4.0.3 @@ -10293,7 +10263,7 @@ packages: - supports-color dev: false - /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint-import-resolver-typescript@3.6.1)(eslint@8.56.0): + /eslint-plugin-import@2.29.1(@typescript-eslint/parser@6.21.0)(eslint@8.56.0): resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==} engines: {node: '>=4'} peerDependencies: @@ -15374,8 +15344,8 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - /pushmodal@1.0.0(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-34JSZHJHGTcLqBgYk9Fyiw5vBYJZrcgoDE7GfHehKKzxBt/Ro2bSLTIGRnzQ+NRv389GxH6WXCBUH+6VJ1wvTg==} + /pushmodal@1.0.3(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-p8HxdCoXfwDU7V3uUxGpbZzK0/nHWey/A8wgfVrUDMSJC3XXEC+Hx8c+UsrTHRXw60ZJSjSoAgldyEh35UPsog==} peerDependencies: '@radix-ui/react-dialog': ^1.0.0 react: ^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0