improved date range selector with hotkeys
This commit is contained in:
@@ -78,7 +78,7 @@
|
|||||||
"nextjs-toploader": "^1.6.11",
|
"nextjs-toploader": "^1.6.11",
|
||||||
"nuqs": "^1.16.1",
|
"nuqs": "^1.16.1",
|
||||||
"prisma-error-enum": "^0.1.3",
|
"prisma-error-enum": "^0.1.3",
|
||||||
"pushmodal": "^1.0.0",
|
"pushmodal": "^1.0.3",
|
||||||
"ramda": "^0.29.1",
|
"ramda": "^0.29.1",
|
||||||
"random-animal-name": "^0.1.1",
|
"random-animal-name": "^0.1.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { toast } from 'sonner';
|
|||||||
import {
|
import {
|
||||||
getDefaultIntervalByDates,
|
getDefaultIntervalByDates,
|
||||||
getDefaultIntervalByRange,
|
getDefaultIntervalByRange,
|
||||||
|
timeWindows,
|
||||||
} from '@openpanel/constants';
|
} from '@openpanel/constants';
|
||||||
import type { getReportsByDashboardId } from '@openpanel/db';
|
import type { getReportsByDashboardId } from '@openpanel/db';
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export function ListReports({ reports }: ListReportsProps) {
|
|||||||
</StickyBelowHeader>
|
</StickyBelowHeader>
|
||||||
<div className="mx-auto flex max-w-3xl flex-col gap-8 p-4 md:p-8">
|
<div className="mx-auto flex max-w-3xl flex-col gap-8 p-4 md:p-8">
|
||||||
{reports.map((report) => {
|
{reports.map((report) => {
|
||||||
const chartRange = report.range; // timeRanges[report.range];
|
const chartRange = report.range;
|
||||||
return (
|
return (
|
||||||
<div className="card" key={report.id}>
|
<div className="card" key={report.id}>
|
||||||
<Link
|
<Link
|
||||||
@@ -84,7 +85,7 @@ export function ListReports({ reports }: ListReportsProps) {
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{chartRange}
|
{timeWindows[chartRange].label}
|
||||||
</span>
|
</span>
|
||||||
{startDate && endDate ? (
|
{startDate && endDate ? (
|
||||||
<span>Custom dates</span>
|
<span>Custom dates</span>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export function EventsPerDayChart({ projectId, filters, events }: Props) {
|
|||||||
<div className="card mb-8 p-4">
|
<div className="card mb-8 p-4">
|
||||||
<ChartSwitchShortcut
|
<ChartSwitchShortcut
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
range="1m"
|
range="30d"
|
||||||
chartType="histogram"
|
chartType="histogram"
|
||||||
events={
|
events={
|
||||||
events && events.length > 0
|
events && events.length > 0
|
||||||
|
|||||||
@@ -1,37 +1,20 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
|
||||||
import { ReportRange } from '@/components/report/ReportRange';
|
import { TimeWindowPicker } from '@/components/time-window-picker';
|
||||||
import { endOfDay, startOfDay } from 'date-fns';
|
|
||||||
|
|
||||||
export function OverviewReportRange() {
|
export function OverviewReportRange() {
|
||||||
const { range, setRange, setEndDate, setStartDate, startDate, endDate } =
|
const { range, setRange, setStartDate, setEndDate, endDate, startDate } =
|
||||||
useOverviewOptions();
|
useOverviewOptions();
|
||||||
return (
|
|
||||||
<ReportRange
|
|
||||||
range={range}
|
|
||||||
onRangeChange={(value) => {
|
|
||||||
setRange(value);
|
|
||||||
setStartDate(null);
|
|
||||||
setEndDate(null);
|
|
||||||
}}
|
|
||||||
dates={{
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
}}
|
|
||||||
onDatesChange={(val) => {
|
|
||||||
if (!val) return;
|
|
||||||
|
|
||||||
if (val.from && val.to) {
|
return (
|
||||||
setRange(null);
|
<TimeWindowPicker
|
||||||
setStartDate(startOfDay(val.from).toISOString());
|
onChange={setRange}
|
||||||
setEndDate(endOfDay(val.to).toISOString());
|
value={range}
|
||||||
} else if (val.from) {
|
onStartDateChange={(date) => setStartDate(date)}
|
||||||
setStartDate(startOfDay(val.from).toISOString());
|
onEndDateChange={(date) => setEndDate(date)}
|
||||||
} else if (val.to) {
|
endDate={endDate}
|
||||||
setEndDate(endOfDay(val.to).toISOString());
|
startDate={startDate}
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default async function Page({
|
|||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
interval: 'day',
|
interval: 'day',
|
||||||
name: 'Events',
|
name: 'Events',
|
||||||
range: '1m',
|
range: '30d',
|
||||||
previous: false,
|
previous: false,
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,11 +6,9 @@ import { ChartSwitch } from '@/components/report/chart';
|
|||||||
import { ReportChartType } from '@/components/report/ReportChartType';
|
import { ReportChartType } from '@/components/report/ReportChartType';
|
||||||
import { ReportInterval } from '@/components/report/ReportInterval';
|
import { ReportInterval } from '@/components/report/ReportInterval';
|
||||||
import { ReportLineType } from '@/components/report/ReportLineType';
|
import { ReportLineType } from '@/components/report/ReportLineType';
|
||||||
import { ReportRange } from '@/components/report/ReportRange';
|
|
||||||
import { ReportSaveButton } from '@/components/report/ReportSaveButton';
|
import { ReportSaveButton } from '@/components/report/ReportSaveButton';
|
||||||
import {
|
import {
|
||||||
changeDateRanges,
|
changeDateRanges,
|
||||||
changeDates,
|
|
||||||
changeEndDate,
|
changeEndDate,
|
||||||
changeStartDate,
|
changeStartDate,
|
||||||
ready,
|
ready,
|
||||||
@@ -18,6 +16,7 @@ import {
|
|||||||
setReport,
|
setReport,
|
||||||
} from '@/components/report/reportSlice';
|
} from '@/components/report/reportSlice';
|
||||||
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
|
import { ReportSidebar } from '@/components/report/sidebar/ReportSidebar';
|
||||||
|
import { TimeWindowPicker } from '@/components/time-window-picker';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
@@ -63,32 +62,20 @@ export default function ReportEditor({
|
|||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<div className="col-span-4 grid grid-cols-2 gap-2 md:grid-cols-4">
|
<div className="col-span-4 grid grid-cols-2 gap-2 md:grid-cols-4">
|
||||||
<ReportChartType className="min-w-0 flex-1" />
|
<ReportChartType className="min-w-0 flex-1" />
|
||||||
<ReportRange
|
<TimeWindowPicker
|
||||||
className="min-w-0 flex-1"
|
className="min-w-0 flex-1"
|
||||||
range={report.range}
|
onChange={(value) => {
|
||||||
onRangeChange={(value) => {
|
|
||||||
dispatch(changeDateRanges(value));
|
dispatch(changeDateRanges(value));
|
||||||
}}
|
}}
|
||||||
dates={{
|
value={report.range}
|
||||||
startDate: report.startDate,
|
onStartDateChange={(date) =>
|
||||||
endDate: report.endDate,
|
dispatch(changeStartDate(startOfDay(date).toISOString()))
|
||||||
}}
|
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
}}
|
onEndDateChange={(date) =>
|
||||||
|
dispatch(changeEndDate(endOfDay(date).toISOString()))
|
||||||
|
}
|
||||||
|
endDate={report.endDate}
|
||||||
|
startDate={report.startDate}
|
||||||
/>
|
/>
|
||||||
<ReportInterval className="min-w-0 flex-1" />
|
<ReportInterval className="min-w-0 flex-1" />
|
||||||
<ReportLineType className="min-w-0 flex-1" />
|
<ReportLineType className="min-w-0 flex-1" />
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
parseAsBoolean,
|
parseAsBoolean,
|
||||||
parseAsInteger,
|
parseAsInteger,
|
||||||
@@ -9,8 +10,9 @@ import {
|
|||||||
import {
|
import {
|
||||||
getDefaultIntervalByDates,
|
getDefaultIntervalByDates,
|
||||||
getDefaultIntervalByRange,
|
getDefaultIntervalByRange,
|
||||||
timeRanges,
|
timeWindows,
|
||||||
} from '@openpanel/constants';
|
} from '@openpanel/constants';
|
||||||
|
import type { IChartRange } from '@openpanel/validation';
|
||||||
import { mapKeys } from '@openpanel/validation';
|
import { mapKeys } from '@openpanel/validation';
|
||||||
|
|
||||||
const nuqsOptions = { history: 'push' } as const;
|
const nuqsOptions = { history: 'push' } as const;
|
||||||
@@ -30,7 +32,7 @@ export function useOverviewOptions() {
|
|||||||
);
|
);
|
||||||
const [range, setRange] = useQueryState(
|
const [range, setRange] = useQueryState(
|
||||||
'range',
|
'range',
|
||||||
parseAsStringEnum(mapKeys(timeRanges))
|
parseAsStringEnum(mapKeys(timeWindows))
|
||||||
.withDefault('7d')
|
.withDefault('7d')
|
||||||
.withOptions(nuqsOptions)
|
.withOptions(nuqsOptions)
|
||||||
);
|
);
|
||||||
@@ -54,7 +56,13 @@ export function useOverviewOptions() {
|
|||||||
previous,
|
previous,
|
||||||
setPrevious,
|
setPrevious,
|
||||||
range,
|
range,
|
||||||
setRange,
|
setRange: (value: IChartRange | null) => {
|
||||||
|
if (value !== 'custom') {
|
||||||
|
setStartDate(null);
|
||||||
|
setEndDate(null);
|
||||||
|
}
|
||||||
|
setRange(value);
|
||||||
|
},
|
||||||
metric,
|
metric,
|
||||||
setMetric,
|
setMetric,
|
||||||
startDate,
|
startDate,
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
isHourIntervalEnabledByRange,
|
isHourIntervalEnabledByRange,
|
||||||
isMinuteIntervalEnabledByRange,
|
isMinuteIntervalEnabledByRange,
|
||||||
} from '@openpanel/constants';
|
} from '@openpanel/constants';
|
||||||
import type { IInterval } from '@openpanel/validation';
|
|
||||||
|
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
import { changeInterval } from './reportSlice';
|
import { changeInterval } from './reportSlice';
|
||||||
@@ -55,10 +54,7 @@ export function ReportInterval({ className }: ReportIntervalProps) {
|
|||||||
value: 'month',
|
value: 'month',
|
||||||
label: 'Month',
|
label: 'Month',
|
||||||
disabled:
|
disabled:
|
||||||
range === 'today' ||
|
range === 'today' || range === 'lastHour' || range === '30min',
|
||||||
range === '24h' ||
|
|
||||||
range === '1h' ||
|
|
||||||
range === '30min',
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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<ExtendedComboboxProps<string>, 'value' | 'onChange'>) {
|
|
||||||
const { isBelowSm } = useBreakpoint('sm');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<Button
|
|
||||||
id="date"
|
|
||||||
variant={'outline'}
|
|
||||||
className={cn('justify-start text-left font-normal', className)}
|
|
||||||
icon={CalendarIcon}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
|
|
||||||
{dates.startDate ? (
|
|
||||||
dates.endDate ? (
|
|
||||||
<>
|
|
||||||
{format(dates.startDate, 'LLL dd')} -{' '}
|
|
||||||
{format(dates.endDate, 'LLL dd')}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
format(dates.startDate, 'LLL dd, y')
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<span>{range}</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<ChevronsUpDownIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
|
||||||
<div className="border-b border-border p-4">
|
|
||||||
<ToggleGroup
|
|
||||||
value={range}
|
|
||||||
onValueChange={(value) => {
|
|
||||||
if (value) onRangeChange(value as IChartRange);
|
|
||||||
}}
|
|
||||||
type="single"
|
|
||||||
variant="outline"
|
|
||||||
className="flex-wrap max-sm:max-w-xs"
|
|
||||||
>
|
|
||||||
{Object.values(timeRanges).map((key) => (
|
|
||||||
<ToggleGroupItem value={key} aria-label={key} key={key}>
|
|
||||||
{key}
|
|
||||||
</ToggleGroupItem>
|
|
||||||
))}
|
|
||||||
</ToggleGroup>
|
|
||||||
</div>
|
|
||||||
<Calendar
|
|
||||||
initialFocus
|
|
||||||
mode="range"
|
|
||||||
defaultMonth={
|
|
||||||
dates.startDate ? new Date(dates.startDate) : new Date()
|
|
||||||
}
|
|
||||||
selected={{
|
|
||||||
from: dates.startDate ? new Date(dates.startDate) : undefined,
|
|
||||||
to: dates.endDate ? new Date(dates.endDate) : undefined,
|
|
||||||
}}
|
|
||||||
onSelect={onDatesChange}
|
|
||||||
numberOfMonths={isBelowSm ? 1 : 2}
|
|
||||||
className="[&_table]:mx-auto [&_table]:w-auto"
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { start } from 'repl';
|
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
import { isSameDay, isSameMonth } from 'date-fns';
|
import { isSameDay, isSameMonth } from 'date-fns';
|
||||||
@@ -39,7 +38,7 @@ const initialState: InitialState = {
|
|||||||
interval: 'day',
|
interval: 'day',
|
||||||
breakdowns: [],
|
breakdowns: [],
|
||||||
events: [],
|
events: [],
|
||||||
range: '1m',
|
range: '30d',
|
||||||
startDate: null,
|
startDate: null,
|
||||||
endDate: null,
|
endDate: null,
|
||||||
previous: false,
|
previous: false,
|
||||||
@@ -232,10 +231,11 @@ export const reportSlice = createSlice({
|
|||||||
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
|
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
state.range = action.payload;
|
state.range = action.payload;
|
||||||
|
if (action.payload !== 'custom') {
|
||||||
state.startDate = null;
|
state.startDate = null;
|
||||||
state.endDate = null;
|
state.endDate = null;
|
||||||
|
|
||||||
state.interval = getDefaultIntervalByRange(action.payload);
|
state.interval = getDefaultIntervalByRange(action.payload);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Formula
|
// Formula
|
||||||
|
|||||||
196
apps/dashboard/src/components/time-window-picker.tsx
Normal file
196
apps/dashboard/src/components/time-window-picker.tsx
Normal file
@@ -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 (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
icon={CalendarIcon}
|
||||||
|
className={cn('justify-start', className)}
|
||||||
|
>
|
||||||
|
{timeWindow?.label}
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent className="w-56">
|
||||||
|
<DropdownMenuLabel>Time window</DropdownMenuLabel>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onClick={() => onChange(timeWindows['30min'].key)}>
|
||||||
|
{timeWindows['30min'].label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows['30min'].shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => onChange(timeWindows.lastHour.key)}>
|
||||||
|
{timeWindows.lastHour.label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows.lastHour.shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => onChange(timeWindows.today.key)}>
|
||||||
|
{timeWindows.today.label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows.today.shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onClick={() => onChange(timeWindows['7d'].key)}>
|
||||||
|
{timeWindows['7d'].label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows['7d'].shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => onChange(timeWindows['30d'].key)}>
|
||||||
|
{timeWindows['30d'].label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows['30d'].shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onChange(timeWindows.monthToDate.key)}
|
||||||
|
>
|
||||||
|
{timeWindows.monthToDate.label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows.monthToDate.shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => onChange(timeWindows.lastMonth.key)}>
|
||||||
|
{timeWindows.lastMonth.label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows.lastMonth.shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onClick={() => onChange(timeWindows.yearToDate.key)}
|
||||||
|
>
|
||||||
|
{timeWindows.yearToDate.label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows.yearToDate.shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onClick={() => onChange(timeWindows.lastYear.key)}>
|
||||||
|
{timeWindows.lastYear.label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows.lastYear.shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
|
||||||
|
<DropdownMenuGroup>
|
||||||
|
<DropdownMenuItem onClick={() => handleCustom()}>
|
||||||
|
{timeWindows.custom.label}
|
||||||
|
<DropdownMenuShortcut>
|
||||||
|
{timeWindows.custom.shortcut}
|
||||||
|
</DropdownMenuShortcut>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuGroup>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ import { Loader2 } from 'lucide-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
|
|||||||
65
apps/dashboard/src/modals/DateRangerPicker.tsx
Normal file
65
apps/dashboard/src/modals/DateRangerPicker.tsx
Normal file
@@ -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 (
|
||||||
|
<ModalContent className="max-w-[600px]">
|
||||||
|
<ModalHeader title="Pick a date range" className="mb-0" />
|
||||||
|
<Calendar
|
||||||
|
initialFocus
|
||||||
|
mode="range"
|
||||||
|
defaultMonth={subMonths(
|
||||||
|
startDate ? new Date(startDate) : new Date(),
|
||||||
|
isBelowSm ? 0 : 1
|
||||||
|
)}
|
||||||
|
selected={{
|
||||||
|
from: startDate,
|
||||||
|
to: endDate,
|
||||||
|
}}
|
||||||
|
toDate={new Date()}
|
||||||
|
onSelect={(range) => {
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="mt-8"
|
||||||
|
onClick={() => {
|
||||||
|
popModal();
|
||||||
|
if (startDate && endDate) {
|
||||||
|
onChange({
|
||||||
|
startDate: startDate,
|
||||||
|
endDate: endDate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
</ModalContent>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { DialogContent } from '@/components/ui/dialog';
|
import { DialogContent } from '@/components/ui/dialog';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
import type { DialogContentProps } from '@radix-ui/react-dialog';
|
||||||
import { X } from 'lucide-react';
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
@@ -19,11 +20,17 @@ interface ModalHeaderProps {
|
|||||||
title: string | React.ReactNode;
|
title: string | React.ReactNode;
|
||||||
text?: string | React.ReactNode;
|
text?: string | React.ReactNode;
|
||||||
onClose?: (() => void) | false;
|
onClose?: (() => void) | false;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ModalHeader({ title, text, onClose }: ModalHeaderProps) {
|
export function ModalHeader({
|
||||||
|
title,
|
||||||
|
text,
|
||||||
|
onClose,
|
||||||
|
className,
|
||||||
|
}: ModalHeaderProps) {
|
||||||
return (
|
return (
|
||||||
<div className="mb-6 flex justify-between">
|
<div className={cn('mb-6 flex justify-between', className)}>
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-0.5 font-medium">{title}</div>
|
<div className="mt-0.5 font-medium">{title}</div>
|
||||||
{!!text && <div className="text-sm text-muted-foreground">{text}</div>}
|
{!!text && <div className="text-sm text-muted-foreground">{text}</div>}
|
||||||
|
|||||||
@@ -59,11 +59,19 @@ const modals = {
|
|||||||
FunnelStepDetails: dynamic(() => import('./FunnelStepDetails'), {
|
FunnelStepDetails: dynamic(() => import('./FunnelStepDetails'), {
|
||||||
loading: Loading,
|
loading: Loading,
|
||||||
}),
|
}),
|
||||||
|
DateRangerPicker: dynamic(() => import('./DateRangerPicker'), {
|
||||||
|
loading: Loading,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { pushModal, popModal, popAllModals, ModalProvider } =
|
export const {
|
||||||
createPushModal({
|
pushModal,
|
||||||
|
popModal,
|
||||||
|
popAllModals,
|
||||||
|
ModalProvider,
|
||||||
|
useOnPushModal,
|
||||||
|
} = createPushModal({
|
||||||
modals,
|
modals,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const showConfirm = (props: ConfirmProps) => pushModal('Confirm', props);
|
export const showConfirm = (props: ConfirmProps) => pushModal('Confirm', props);
|
||||||
|
|||||||
@@ -2,6 +2,59 @@ import { isSameDay, isSameMonth } from 'date-fns';
|
|||||||
|
|
||||||
export const NOT_SET_VALUE = '(not set)';
|
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 = {
|
export const ProjectTypeNames = {
|
||||||
website: 'Website',
|
website: 'Website',
|
||||||
app: 'App',
|
app: 'App',
|
||||||
@@ -64,7 +117,7 @@ export const alphabetIds = [
|
|||||||
'J',
|
'J',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const timeRanges = {
|
export const deprecated_timeRanges = {
|
||||||
'30min': '30min',
|
'30min': '30min',
|
||||||
'1h': '1h',
|
'1h': '1h',
|
||||||
today: 'today',
|
today: 'today',
|
||||||
@@ -84,26 +137,29 @@ export const metrics = {
|
|||||||
max: 'max',
|
max: 'max',
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export function isMinuteIntervalEnabledByRange(range: keyof typeof timeRanges) {
|
export function isMinuteIntervalEnabledByRange(
|
||||||
return range === '30min' || range === '1h';
|
range: keyof typeof timeWindows
|
||||||
|
) {
|
||||||
|
return range === '30min' || range === 'lastHour';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isHourIntervalEnabledByRange(range: keyof typeof timeRanges) {
|
export function isHourIntervalEnabledByRange(range: keyof typeof timeWindows) {
|
||||||
return (
|
return isMinuteIntervalEnabledByRange(range) || range === 'today';
|
||||||
isMinuteIntervalEnabledByRange(range) ||
|
|
||||||
range === 'today' ||
|
|
||||||
range === '24h'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultIntervalByRange(
|
export function getDefaultIntervalByRange(
|
||||||
range: keyof typeof timeRanges
|
range: keyof typeof timeWindows
|
||||||
): keyof typeof intervals {
|
): keyof typeof intervals {
|
||||||
if (range === '30min' || range === '1h') {
|
if (range === '30min' || range === 'lastHour') {
|
||||||
return 'minute';
|
return 'minute';
|
||||||
} else if (range === 'today' || range === '24h') {
|
} else if (range === 'today') {
|
||||||
return 'hour';
|
return 'hour';
|
||||||
} else if (range === '7d' || range === '14d' || range === '1m') {
|
} else if (
|
||||||
|
range === '7d' ||
|
||||||
|
range === '30d' ||
|
||||||
|
range === 'lastMonth' ||
|
||||||
|
range === 'monthToDate'
|
||||||
|
) {
|
||||||
return 'day';
|
return 'day';
|
||||||
}
|
}
|
||||||
return 'month';
|
return 'month';
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "reports" ALTER COLUMN "range" SET DEFAULT '30d';
|
||||||
@@ -163,7 +163,7 @@ model Report {
|
|||||||
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
|
||||||
name String
|
name String
|
||||||
interval Interval
|
interval Interval
|
||||||
range String @default("1m")
|
range String @default("30d")
|
||||||
chartType ChartType
|
chartType ChartType
|
||||||
lineType String @default("monotone")
|
lineType String @default("monotone")
|
||||||
breakdowns Json
|
breakdowns Json
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import { alphabetIds, lineTypes, timeRanges } from '@openpanel/constants';
|
import {
|
||||||
|
alphabetIds,
|
||||||
|
deprecated_timeRanges,
|
||||||
|
lineTypes,
|
||||||
|
} from '@openpanel/constants';
|
||||||
import type {
|
import type {
|
||||||
IChartBreakdown,
|
IChartBreakdown,
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
@@ -52,7 +56,10 @@ export function transformReport(
|
|||||||
lineType: (report.lineType as IChartLineType) ?? lineTypes.monotone,
|
lineType: (report.lineType as IChartLineType) ?? lineTypes.monotone,
|
||||||
interval: report.interval,
|
interval: report.interval,
|
||||||
name: report.name || 'Untitled',
|
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,
|
previous: report.previous ?? false,
|
||||||
formula: report.formula ?? undefined,
|
formula: report.formula ?? undefined,
|
||||||
metric: report.metric ?? 'sum',
|
metric: report.metric ?? 'sum',
|
||||||
|
|||||||
@@ -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 * as mathjs from 'mathjs';
|
||||||
import { repeat, reverse, sort } from 'ramda';
|
import { repeat, reverse, sort } from 'ramda';
|
||||||
import { escape } from 'sqlstring';
|
import { escape } from 'sqlstring';
|
||||||
@@ -298,22 +308,9 @@ export async function getChartData(payload: IGetChartDataInput) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getDatesFromRange(range: IChartRange) {
|
export function getDatesFromRange(range: IChartRange) {
|
||||||
if (range === 'today') {
|
if (range === '30min' || range === 'lastHour') {
|
||||||
const startDate = new Date();
|
const minutes = range === '30min' ? 30 : 60;
|
||||||
const endDate = new Date();
|
const startDate = subMinutes(new Date(), minutes).toUTCString();
|
||||||
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();
|
|
||||||
const endDate = new Date().toUTCString();
|
const endDate = new Date().toUTCString();
|
||||||
|
|
||||||
return {
|
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 {
|
return {
|
||||||
startDate: startDate.toUTCString(),
|
startDate: startDate.toUTCString(),
|
||||||
endDate: endDate.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);
|
if (range === '7d') {
|
||||||
startDate.setUTCHours(0, 0, 0, 0);
|
const startDate = subDays(new Date(), 7).toUTCString();
|
||||||
const endDate = new Date();
|
const endDate = new Date().toUTCString();
|
||||||
endDate.setUTCHours(23, 59, 59, 999);
|
|
||||||
return {
|
return {
|
||||||
startDate: startDate.toUTCString(),
|
startDate,
|
||||||
endDate: endDate.toUTCString(),
|
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: subDays(new Date(), 30).toISOString(),
|
||||||
|
endDate: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,44 +427,7 @@ export function getChartPrevStartEndDate({
|
|||||||
endDate: string;
|
endDate: string;
|
||||||
range: IChartRange;
|
range: IChartRange;
|
||||||
}) {
|
}) {
|
||||||
let diff = 0;
|
const diff = new Date(endDate).getTime() - new Date(startDate).getTime();
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startDate: new Date(new Date(startDate).getTime() - diff).toISOString(),
|
startDate: new Date(new Date(startDate).getTime() - diff).toISOString(),
|
||||||
endDate: new Date(new Date(endDate).getTime() - diff).toISOString(),
|
endDate: new Date(new Date(endDate).getTime() - diff).toISOString(),
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export const reportRouter = createTRPCRouter({
|
|||||||
breakdowns: report.breakdowns,
|
breakdowns: report.breakdowns,
|
||||||
chartType: report.chartType,
|
chartType: report.chartType,
|
||||||
lineType: report.lineType,
|
lineType: report.lineType,
|
||||||
range: report.range,
|
range: report.range === 'custom' ? '30d' : report.range,
|
||||||
formula: report.formula,
|
formula: report.formula,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -53,7 +53,7 @@ export const reportRouter = createTRPCRouter({
|
|||||||
breakdowns: report.breakdowns,
|
breakdowns: report.breakdowns,
|
||||||
chartType: report.chartType,
|
chartType: report.chartType,
|
||||||
lineType: report.lineType,
|
lineType: report.lineType,
|
||||||
range: report.range,
|
range: report.range === 'custom' ? '30d' : report.range,
|
||||||
formula: report.formula,
|
formula: report.formula,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
lineTypes,
|
lineTypes,
|
||||||
metrics,
|
metrics,
|
||||||
operators,
|
operators,
|
||||||
timeRanges,
|
timeWindows,
|
||||||
} from '@openpanel/constants';
|
} from '@openpanel/constants';
|
||||||
|
|
||||||
export function objectToZodEnums<K extends string>(
|
export function objectToZodEnums<K extends string>(
|
||||||
@@ -57,7 +57,7 @@ export const zTimeInterval = z.enum(objectToZodEnums(intervals));
|
|||||||
|
|
||||||
export const zMetric = z.enum(objectToZodEnums(metrics));
|
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({
|
export const zChartInput = z.object({
|
||||||
name: z.string().default(''),
|
name: z.string().default(''),
|
||||||
@@ -66,7 +66,7 @@ export const zChartInput = z.object({
|
|||||||
interval: zTimeInterval.default('day'),
|
interval: zTimeInterval.default('day'),
|
||||||
events: zChartEvents,
|
events: zChartEvents,
|
||||||
breakdowns: zChartBreakdowns.default([]),
|
breakdowns: zChartBreakdowns.default([]),
|
||||||
range: zRange.default('1m'),
|
range: zRange.default('30d'),
|
||||||
previous: z.boolean().default(false),
|
previous: z.boolean().default(false),
|
||||||
formula: z.string().optional(),
|
formula: z.string().optional(),
|
||||||
metric: zMetric.default('sum'),
|
metric: zMetric.default('sum'),
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import type { timeRanges } from '@openpanel/constants';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
zChartBreakdown,
|
zChartBreakdown,
|
||||||
zChartEvent,
|
zChartEvent,
|
||||||
@@ -9,6 +7,7 @@ import type {
|
|||||||
zChartType,
|
zChartType,
|
||||||
zLineType,
|
zLineType,
|
||||||
zMetric,
|
zMetric,
|
||||||
|
zRange,
|
||||||
zTimeInterval,
|
zTimeInterval,
|
||||||
} from './index';
|
} from './index';
|
||||||
|
|
||||||
@@ -24,7 +23,7 @@ export type IInterval = z.infer<typeof zTimeInterval>;
|
|||||||
export type IChartType = z.infer<typeof zChartType>;
|
export type IChartType = z.infer<typeof zChartType>;
|
||||||
export type IChartMetric = z.infer<typeof zMetric>;
|
export type IChartMetric = z.infer<typeof zMetric>;
|
||||||
export type IChartLineType = z.infer<typeof zLineType>;
|
export type IChartLineType = z.infer<typeof zLineType>;
|
||||||
export type IChartRange = keyof typeof timeRanges;
|
export type IChartRange = z.infer<typeof zRange>;
|
||||||
export type IGetChartDataInput = {
|
export type IGetChartDataInput = {
|
||||||
event: IChartEvent;
|
event: IChartEvent;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
50
pnpm-lock.yaml
generated
50
pnpm-lock.yaml
generated
@@ -334,8 +334,8 @@ importers:
|
|||||||
specifier: ^0.1.3
|
specifier: ^0.1.3
|
||||||
version: 0.1.3
|
version: 0.1.3
|
||||||
pushmodal:
|
pushmodal:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.3
|
||||||
version: 1.0.0(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.0.3(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0)
|
||||||
ramda:
|
ramda:
|
||||||
specifier: ^0.29.1
|
specifier: ^0.29.1
|
||||||
version: 0.29.1
|
version: 0.29.1
|
||||||
@@ -10085,7 +10085,7 @@ packages:
|
|||||||
eslint: 8.56.0
|
eslint: 8.56.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
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-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-jsx-a11y: 6.8.0(eslint@8.56.0)
|
||||||
eslint-plugin-react: 7.33.2(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)
|
eslint-plugin-react-hooks: 4.6.0(eslint@8.56.0)
|
||||||
@@ -10166,7 +10166,7 @@ packages:
|
|||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.3.4
|
debug: 4.3.4
|
||||||
eslint: 8.56.0
|
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
|
glob: 7.2.3
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
@@ -10198,36 +10198,6 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
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):
|
/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==}
|
resolution: {integrity: sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -10258,7 +10228,7 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -10268,7 +10238,7 @@ packages:
|
|||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
optional: true
|
optional: true
|
||||||
dependencies:
|
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-includes: 3.1.7
|
||||||
array.prototype.findlastindex: 1.2.4
|
array.prototype.findlastindex: 1.2.4
|
||||||
array.prototype.flat: 1.3.2
|
array.prototype.flat: 1.3.2
|
||||||
@@ -10277,7 +10247,7 @@ packages:
|
|||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.56.0
|
eslint: 8.56.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
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
|
hasown: 2.0.1
|
||||||
is-core-module: 2.13.1
|
is-core-module: 2.13.1
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -10293,7 +10263,7 @@ packages:
|
|||||||
- supports-color
|
- supports-color
|
||||||
dev: false
|
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==}
|
resolution: {integrity: sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -15374,8 +15344,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
/pushmodal@1.0.0(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0):
|
/pushmodal@1.0.3(@radix-ui/react-dialog@1.0.5)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-34JSZHJHGTcLqBgYk9Fyiw5vBYJZrcgoDE7GfHehKKzxBt/Ro2bSLTIGRnzQ+NRv389GxH6WXCBUH+6VJ1wvTg==}
|
resolution: {integrity: sha512-p8HxdCoXfwDU7V3uUxGpbZzK0/nHWey/A8wgfVrUDMSJC3XXEC+Hx8c+UsrTHRXw60ZJSjSoAgldyEh35UPsog==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@radix-ui/react-dialog': ^1.0.0
|
'@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
|
react: ^16.12.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0
|
||||||
|
|||||||
Reference in New Issue
Block a user