improved date range selector with hotkeys

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-04-27 09:52:24 +02:00
parent e2d56fb34f
commit 4059dad727
24 changed files with 520 additions and 319 deletions

View File

@@ -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';

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "reports" ALTER COLUMN "range" SET DEFAULT '30d';

View File

@@ -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

View File

@@ -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',

View File

@@ -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(),

View File

@@ -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,
},
});

View File

@@ -6,7 +6,7 @@ import {
lineTypes,
metrics,
operators,
timeRanges,
timeWindows,
} from '@openpanel/constants';
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 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'),

View File

@@ -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<typeof zTimeInterval>;
export type IChartType = z.infer<typeof zChartType>;
export type IChartMetric = z.infer<typeof zMetric>;
export type IChartLineType = z.infer<typeof zLineType>;
export type IChartRange = keyof typeof timeRanges;
export type IChartRange = z.infer<typeof zRange>;
export type IGetChartDataInput = {
event: IChartEvent;
projectId: string;