ability to use custom dates everywhere

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-02-26 16:40:09 +01:00
parent 94a0ac7bd0
commit 6a8656da3d
19 changed files with 254 additions and 95 deletions

View File

@@ -1,9 +1,8 @@
'use client'; 'use client';
import { useState } from 'react';
import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header'; import { StickyBelowHeader } from '@/app/(app)/[organizationId]/[projectId]/layout-sticky-below-header';
import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { LazyChart } from '@/components/report/chart/LazyChart'; import { LazyChart } from '@/components/report/chart/LazyChart';
import { ReportRange } from '@/components/report/ReportRange';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
DropdownMenu, DropdownMenu,
@@ -18,9 +17,13 @@ import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { getDefaultIntervalByRange } from '@mixan/constants'; import {
getDefaultIntervalByDates,
getDefaultIntervalByRange,
} from '@mixan/constants';
import type { getReportsByDashboardId } from '@mixan/db'; import type { getReportsByDashboardId } from '@mixan/db';
import type { IChartRange } from '@mixan/validation';
import { OverviewReportRange } from '../../overview-sticky-header';
interface ListReportsProps { interface ListReportsProps {
reports: Awaited<ReturnType<typeof getReportsByDashboardId>>; reports: Awaited<ReturnType<typeof getReportsByDashboardId>>;
@@ -29,16 +32,12 @@ interface ListReportsProps {
export function ListReports({ reports }: ListReportsProps) { export function ListReports({ reports }: ListReportsProps) {
const router = useRouter(); const router = useRouter();
const params = useAppParams<{ dashboardId: string }>(); const params = useAppParams<{ dashboardId: string }>();
const [range, setRange] = useState<null | IChartRange>(null); const { range, startDate, endDate } = useOverviewOptions();
return ( return (
<> <>
<StickyBelowHeader className="p-4 items-center justify-between flex"> <StickyBelowHeader className="p-4 items-center justify-between flex">
<ReportRange <OverviewReportRange />
placeholder="Override range"
value={range}
onChange={(value) => setRange((p) => (p === value ? null : value))}
/>
<Button <Button
icon={PlusIcon} icon={PlusIcon}
onClick={() => { onClick={() => {
@@ -72,10 +71,20 @@ export function ListReports({ reports }: ListReportsProps) {
<div className="font-medium">{report.name}</div> <div className="font-medium">{report.name}</div>
{chartRange !== null && ( {chartRange !== null && (
<div className="mt-2 text-sm flex gap-2"> <div className="mt-2 text-sm flex gap-2">
<span className={range !== null ? 'line-through' : ''}> <span
className={
range !== null || (startDate && endDate)
? 'line-through'
: ''
}
>
{chartRange} {chartRange}
</span> </span>
{range !== null && <span>{range}</span>} {startDate && endDate ? (
<span>Custom dates</span>
) : (
range !== null && <span>{range}</span>
)}
</div> </div>
)} )}
</div> </div>
@@ -116,8 +125,11 @@ export function ListReports({ reports }: ListReportsProps) {
<LazyChart <LazyChart
{...report} {...report}
range={range ?? report.range} range={range ?? report.range}
startDate={startDate}
endDate={endDate}
interval={ interval={
range ? getDefaultIntervalByRange(range) : report.interval getDefaultIntervalByDates(startDate, endDate) ||
(range ? getDefaultIntervalByRange(range) : report.interval)
} }
editMode={false} editMode={false}
/> />

View File

@@ -14,13 +14,16 @@ interface OverviewMetricsProps {
} }
export default function OverviewMetrics({ projectId }: OverviewMetricsProps) { export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
const { previous, range, interval, metric, setMetric } = useOverviewOptions(); const { previous, range, interval, metric, setMetric, startDate, endDate } =
useOverviewOptions();
const [filters] = useEventQueryFilters(); const [filters] = useEventQueryFilters();
const reports = [ const reports = [
{ {
id: 'Visitors', id: 'Visitors',
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'user', segment: 'user',
@@ -42,6 +45,8 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
{ {
id: 'Sessions', id: 'Sessions',
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -63,6 +68,8 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
{ {
id: 'Pageviews', id: 'Pageviews',
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -84,6 +91,8 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
{ {
id: 'Views per session', id: 'Views per session',
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'user_average', segment: 'user_average',
@@ -105,6 +114,8 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
{ {
id: 'Bounce rate', id: 'Bounce rate',
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -143,6 +154,8 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
{ {
id: 'Visit duration', id: 'Visit duration',
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'property_average', segment: 'property_average',

View File

@@ -2,8 +2,36 @@
import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; import { useOverviewOptions } from '@/components/overview/useOverviewOptions';
import { ReportRange } from '@/components/report/ReportRange'; import { ReportRange } from '@/components/report/ReportRange';
import { endOfDay, startOfDay } from 'date-fns';
export function OverviewReportRange() { export function OverviewReportRange() {
const { range, setRange } = useOverviewOptions(); const { range, setRange, setEndDate, setStartDate, startDate, endDate } =
return <ReportRange value={range} onChange={(value) => setRange(value)} />; 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) {
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());
}
}}
/>
);
} }

View File

@@ -9,7 +9,6 @@ import OverviewTopEvents from '@/components/overview/overview-top-events';
import OverviewTopGeo from '@/components/overview/overview-top-geo'; import OverviewTopGeo from '@/components/overview/overview-top-geo';
import OverviewTopPages from '@/components/overview/overview-top-pages'; import OverviewTopPages from '@/components/overview/overview-top-pages';
import OverviewTopSources from '@/components/overview/overview-top-sources'; import OverviewTopSources from '@/components/overview/overview-top-sources';
import { Dialog } from '@/components/ui/dialog';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { db } from '@mixan/db'; import { db } from '@mixan/db';

View File

@@ -1,10 +1,8 @@
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout'; import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
import { ListProperties } from '@/components/events/ListProperties';
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
import { ProfileAvatar } from '@/components/profiles/ProfileAvatar'; import { ProfileAvatar } from '@/components/profiles/ProfileAvatar';
import { ChartSwitch } from '@/components/report/chart'; import { ChartSwitch } from '@/components/report/chart';
import { GradientBackground } from '@/components/ui/gradient-background';
import { KeyValue } from '@/components/ui/key-value'; import { KeyValue } from '@/components/ui/key-value';
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget'; import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
import { import {
@@ -12,25 +10,21 @@ import {
eventQueryNamesFilter, eventQueryNamesFilter,
} from '@/hooks/useEventQueryFilters'; } from '@/hooks/useEventQueryFilters';
import { getExists } from '@/server/pageExists'; import { getExists } from '@/server/pageExists';
import { formatDateTime } from '@/utils/date';
import { getProfileName } from '@/utils/getters'; import { getProfileName } from '@/utils/getters';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { parseAsInteger } from 'nuqs'; import { parseAsInteger, parseAsString } from 'nuqs';
import type { GetEventListOptions } from '@mixan/db'; import type { GetEventListOptions } from '@mixan/db';
import { import {
getConversionEventNames, getConversionEventNames,
getEventList, getEventList,
getEventMeta,
getEventsCount, getEventsCount,
getProfileById, getProfileById,
getProfilesByExternalId,
} from '@mixan/db'; } from '@mixan/db';
import type { IChartInput } from '@mixan/validation'; import type { IChartEvent, IChartInput } from '@mixan/validation';
import { EventList } from '../../events/event-list'; import { EventList } from '../../events/event-list';
import { StickyBelowHeader } from '../../layout-sticky-below-header'; import { StickyBelowHeader } from '../../layout-sticky-below-header';
import ListProfileEvents from './list-profile-events';
interface PageProps { interface PageProps {
params: { params: {
@@ -42,6 +36,8 @@ interface PageProps {
events?: string; events?: string;
cursor?: string; cursor?: string;
f?: string; f?: string;
startDate: string;
endDate: string;
}; };
} }
@@ -57,6 +53,8 @@ export default async function Page({
events: eventQueryNamesFilter.parse(searchParams.events ?? ''), events: eventQueryNamesFilter.parse(searchParams.events ?? ''),
filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined, filters: eventQueryFiltersParser.parse(searchParams.f ?? '') ?? undefined,
}; };
const startDate = parseAsString.parse(searchParams.startDate);
const endDate = parseAsString.parse(searchParams.endDate);
const [profile, events, count, conversions] = await Promise.all([ const [profile, events, count, conversions] = await Promise.all([
getProfileById(profileId), getProfileById(profileId),
getEventList(eventListOptions), getEventList(eventListOptions),
@@ -65,7 +63,7 @@ export default async function Page({
getExists(organizationId, projectId), getExists(organizationId, projectId),
]); ]);
const chartSelectedEvents = [ const chartSelectedEvents: IChartEvent[] = [
{ {
segment: 'event', segment: 'event',
filters: [ filters: [
@@ -107,6 +105,8 @@ export default async function Page({
const profileChart: IChartInput = { const profileChart: IChartInput = {
projectId, projectId,
startDate,
endDate,
chartType: 'histogram', chartType: 'histogram',
events: chartSelectedEvents, events: chartSelectedEvents,
breakdowns: [], breakdowns: [],

View File

@@ -10,6 +10,9 @@ import { ReportRange } from '@/components/report/ReportRange';
import { ReportSaveButton } from '@/components/report/ReportSaveButton'; import { ReportSaveButton } from '@/components/report/ReportSaveButton';
import { import {
changeDateRanges, changeDateRanges,
changeDates,
changeEndDate,
changeStartDate,
ready, ready,
reset, reset,
setReport, setReport,
@@ -19,6 +22,7 @@ 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';
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import { endOfDay, startOfDay } from 'date-fns';
import { GanttChartSquareIcon } from 'lucide-react'; import { GanttChartSquareIcon } from 'lucide-react';
import type { IServiceReport } from '@mixan/db'; import type { IServiceReport } from '@mixan/db';
@@ -61,10 +65,30 @@ export default function ReportEditor({
<ReportChartType className="min-w-0 flex-1" /> <ReportChartType className="min-w-0 flex-1" />
<ReportRange <ReportRange
className="min-w-0 flex-1" className="min-w-0 flex-1"
value={report.range} range={report.range}
onChange={(value) => { onRangeChange={(value) => {
dispatch(changeDateRanges(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()));
}
}}
/> />
<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" />

View File

@@ -15,7 +15,8 @@ interface OverviewTopDevicesProps {
export default function OverviewTopDevices({ export default function OverviewTopDevices({
projectId, projectId,
}: OverviewTopDevicesProps) { }: OverviewTopDevicesProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters(); const [filters, setFilter] = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('tech', { const [widget, setWidget, widgets] = useOverviewWidget('tech', {
devices: { devices: {
@@ -23,6 +24,8 @@ export default function OverviewTopDevices({
btn: 'Devices', btn: 'Devices',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'user', segment: 'user',
@@ -51,6 +54,8 @@ export default function OverviewTopDevices({
btn: 'Browser', btn: 'Browser',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'user', segment: 'user',
@@ -79,6 +84,8 @@ export default function OverviewTopDevices({
btn: 'Browser Version', btn: 'Browser Version',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'user', segment: 'user',
@@ -107,6 +114,8 @@ export default function OverviewTopDevices({
btn: 'OS', btn: 'OS',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'user', segment: 'user',
@@ -135,6 +144,8 @@ export default function OverviewTopDevices({
btn: 'OS Version', btn: 'OS Version',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'user', segment: 'user',

View File

@@ -15,7 +15,8 @@ interface OverviewTopEventsProps {
export default function OverviewTopEvents({ export default function OverviewTopEvents({
projectId, projectId,
}: OverviewTopEventsProps) { }: OverviewTopEventsProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
const [filters] = useEventQueryFilters(); const [filters] = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('ev', { const [widget, setWidget, widgets] = useOverviewWidget('ev', {
all: { all: {
@@ -23,6 +24,8 @@ export default function OverviewTopEvents({
btn: 'All', btn: 'All',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',

View File

@@ -13,7 +13,8 @@ interface OverviewTopGeoProps {
projectId: string; projectId: string;
} }
export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) { export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters(); const [filters, setFilter] = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('geo', { const [widget, setWidget, widgets] = useOverviewWidget('geo', {
map: { map: {
@@ -21,6 +22,8 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
btn: 'Map', btn: 'Map',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -49,6 +52,8 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
btn: 'Countries', btn: 'Countries',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -77,6 +82,8 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
btn: 'Regions', btn: 'Regions',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -105,6 +112,8 @@ export default function OverviewTopGeo({ projectId }: OverviewTopGeoProps) {
btn: 'Cities', btn: 'Cities',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',

View File

@@ -13,7 +13,8 @@ interface OverviewTopPagesProps {
projectId: string; projectId: string;
} }
export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) { export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters(); const [filters, setFilter] = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('pages', { const [widget, setWidget, widgets] = useOverviewWidget('pages', {
top: { top: {
@@ -21,6 +22,8 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
btn: 'Top pages', btn: 'Top pages',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -49,6 +52,8 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
btn: 'Entries', btn: 'Entries',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -77,6 +82,8 @@ export default function OverviewTopPages({ projectId }: OverviewTopPagesProps) {
btn: 'Exits', btn: 'Exits',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',

View File

@@ -15,7 +15,8 @@ interface OverviewTopSourcesProps {
export default function OverviewTopSources({ export default function OverviewTopSources({
projectId, projectId,
}: OverviewTopSourcesProps) { }: OverviewTopSourcesProps) {
const { interval, range, previous } = useOverviewOptions(); const { interval, range, previous, startDate, endDate } =
useOverviewOptions();
const [filters, setFilter] = useEventQueryFilters(); const [filters, setFilter] = useEventQueryFilters();
const [widget, setWidget, widgets] = useOverviewWidget('sources', { const [widget, setWidget, widgets] = useOverviewWidget('sources', {
all: { all: {
@@ -23,6 +24,8 @@ export default function OverviewTopSources({
btn: 'All', btn: 'All',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -51,6 +54,8 @@ export default function OverviewTopSources({
btn: 'URLs', btn: 'URLs',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -79,6 +84,8 @@ export default function OverviewTopSources({
btn: 'Types', btn: 'Types',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -107,6 +114,8 @@ export default function OverviewTopSources({
btn: 'Source', btn: 'Source',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -135,6 +144,8 @@ export default function OverviewTopSources({
btn: 'Medium', btn: 'Medium',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -163,6 +174,8 @@ export default function OverviewTopSources({
btn: 'Campaign', btn: 'Campaign',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -191,6 +204,8 @@ export default function OverviewTopSources({
btn: 'Term', btn: 'Term',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',
@@ -219,6 +234,8 @@ export default function OverviewTopSources({
btn: 'Content', btn: 'Content',
chart: { chart: {
projectId, projectId,
startDate,
endDate,
events: [ events: [
{ {
segment: 'event', segment: 'event',

View File

@@ -1,11 +1,17 @@
import { useEffect } from 'react';
import { import {
parseAsBoolean, parseAsBoolean,
parseAsInteger, parseAsInteger,
parseAsString,
parseAsStringEnum, parseAsStringEnum,
useQueryState, useQueryState,
} from 'nuqs'; } from 'nuqs';
import { getDefaultIntervalByRange, timeRanges } from '@mixan/constants'; import {
getDefaultIntervalByDates,
getDefaultIntervalByRange,
timeRanges,
} from '@mixan/constants';
import { mapKeys } from '@mixan/validation'; import { mapKeys } from '@mixan/validation';
const nuqsOptions = { history: 'push' } as const; const nuqsOptions = { history: 'push' } as const;
@@ -15,13 +21,25 @@ export function useOverviewOptions() {
'compare', 'compare',
parseAsBoolean.withDefault(true).withOptions(nuqsOptions) parseAsBoolean.withDefault(true).withOptions(nuqsOptions)
); );
const [startDate, setStartDate] = useQueryState(
'start',
parseAsString.withOptions(nuqsOptions)
);
const [endDate, setEndDate] = useQueryState(
'end',
parseAsString.withOptions(nuqsOptions)
);
const [range, setRange] = useQueryState( const [range, setRange] = useQueryState(
'range', 'range',
parseAsStringEnum(mapKeys(timeRanges)) parseAsStringEnum(mapKeys(timeRanges))
.withDefault('7d') .withDefault('7d')
.withOptions(nuqsOptions) .withOptions(nuqsOptions)
); );
const interval = getDefaultIntervalByRange(range);
const interval =
getDefaultIntervalByDates(startDate, endDate) ||
getDefaultIntervalByRange(range);
const [metric, setMetric] = useQueryState( const [metric, setMetric] = useQueryState(
'metric', 'metric',
parseAsInteger.withDefault(0).withOptions(nuqsOptions) parseAsInteger.withDefault(0).withOptions(nuqsOptions)
@@ -40,6 +58,10 @@ export function useOverviewOptions() {
setRange, setRange,
metric, metric,
setMetric, setMetric,
startDate,
setStartDate,
endDate,
setEndDate,
// Computed // Computed
interval, interval,

View File

@@ -7,48 +7,32 @@ import {
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover'; } from '@/components/ui/popover';
import { useBreakpoint } from '@/hooks/useBreakpoint'; import { useBreakpoint } from '@/hooks/useBreakpoint';
import { pushModal } from '@/modals';
import { useDispatch, useSelector } from '@/redux'; import { useDispatch, useSelector } from '@/redux';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { addDays, endOfDay, format, startOfDay, subDays } from 'date-fns'; import { endOfDay, format, startOfDay } from 'date-fns';
import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react'; import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react';
import type { DateRange, SelectRangeEventHandler } from 'react-day-picker'; import type { SelectRangeEventHandler } from 'react-day-picker';
import { timeRanges } from '@mixan/constants'; import { timeRanges } from '@mixan/constants';
import type { IChartRange } from '@mixan/validation'; import type { IChartRange } from '@mixan/validation';
import type { ExtendedComboboxProps } from '../ui/combobox'; import type { ExtendedComboboxProps } from '../ui/combobox';
import { Combobox } from '../ui/combobox';
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group'; import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
import { changeDates, changeEndDate, changeStartDate } from './reportSlice'; import { changeDates, changeEndDate, changeStartDate } from './reportSlice';
export function ReportRange({ export function ReportRange({
onChange, range,
value, onRangeChange,
onDatesChange,
dates,
className, className,
...props ...props
}: ExtendedComboboxProps<IChartRange>) { }: {
const dispatch = useDispatch(); range: IChartRange;
const startDate = useSelector((state) => state.report.startDate); onRangeChange: (range: IChartRange) => void;
const endDate = useSelector((state) => state.report.endDate); onDatesChange: SelectRangeEventHandler;
dates: { startDate: string | null; endDate: string | null };
const setDate: SelectRangeEventHandler = (val) => { } & Omit<ExtendedComboboxProps<string>, 'value' | 'onChange'>) {
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()));
}
};
const { isBelowSm } = useBreakpoint('sm'); const { isBelowSm } = useBreakpoint('sm');
return ( return (
@@ -63,16 +47,17 @@ export function ReportRange({
{...props} {...props}
> >
<span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap"> <span className="min-w-0 overflow-hidden text-ellipsis whitespace-nowrap">
{startDate ? ( {dates.startDate ? (
endDate ? ( dates.endDate ? (
<> <>
{format(startDate, 'LLL dd')} - {format(endDate, 'LLL dd')} {format(dates.startDate, 'LLL dd')} -{' '}
{format(dates.endDate, 'LLL dd')}
</> </>
) : ( ) : (
format(startDate, 'LLL dd, y') format(dates.startDate, 'LLL dd, y')
) )
) : ( ) : (
<span>{value}</span> <span>{range}</span>
)} )}
</span> </span>
<ChevronsUpDownIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" /> <ChevronsUpDownIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
@@ -81,9 +66,9 @@ export function ReportRange({
<PopoverContent className="w-auto p-0" align="start"> <PopoverContent className="w-auto p-0" align="start">
<div className="p-4 border-b border-border"> <div className="p-4 border-b border-border">
<ToggleGroup <ToggleGroup
value={value} value={range}
onValueChange={(value) => { onValueChange={(value) => {
if (value) onChange(value); if (value) onRangeChange(value as IChartRange);
}} }}
type="single" type="single"
variant="outline" variant="outline"
@@ -99,12 +84,14 @@ export function ReportRange({
<Calendar <Calendar
initialFocus initialFocus
mode="range" mode="range"
defaultMonth={startDate ? new Date(startDate) : new Date()} defaultMonth={
dates.startDate ? new Date(dates.startDate) : new Date()
}
selected={{ selected={{
from: startDate ? new Date(startDate) : undefined, from: dates.startDate ? new Date(dates.startDate) : undefined,
to: endDate ? new Date(endDate) : undefined, to: dates.endDate ? new Date(dates.endDate) : undefined,
}} }}
onSelect={setDate} onSelect={onDatesChange}
numberOfMonths={isBelowSm ? 1 : 2} numberOfMonths={isBelowSm ? 1 : 2}
className="[&_table]:mx-auto [&_table]:w-auto" className="[&_table]:mx-auto [&_table]:w-auto"
/> />

View File

@@ -5,6 +5,7 @@ import { isSameDay, isSameMonth } from 'date-fns';
import { import {
alphabetIds, alphabetIds,
getDefaultIntervalByDates,
getDefaultIntervalByRange, getDefaultIntervalByRange,
isHourIntervalEnabledByRange, isHourIntervalEnabledByRange,
isMinuteIntervalEnabledByRange, isMinuteIntervalEnabledByRange,
@@ -39,8 +40,8 @@ const initialState: InitialState = {
breakdowns: [], breakdowns: [],
events: [], events: [],
range: '1m', range: '1m',
startDate: new Date('2024-02-24 00:00:00').toISOString(), startDate: null,
endDate: new Date('2024-02-24 23:59:59').toISOString(), endDate: null,
previous: false, previous: false,
formula: undefined, formula: undefined,
unit: undefined, unit: undefined,
@@ -205,14 +206,12 @@ export const reportSlice = createSlice({
state.dirty = true; state.dirty = true;
state.startDate = action.payload; state.startDate = action.payload;
if (state.startDate && state.endDate) { const interval = getDefaultIntervalByDates(
if (isSameDay(state.startDate, state.endDate)) { state.startDate,
state.interval = 'hour'; state.endDate
} else if (isSameMonth(state.startDate, state.endDate)) { );
state.interval = 'day'; if (interval) {
} else { state.interval = interval;
state.interval = 'month';
}
} }
}, },
@@ -221,14 +220,12 @@ export const reportSlice = createSlice({
state.dirty = true; state.dirty = true;
state.endDate = action.payload; state.endDate = action.payload;
if (state.startDate && state.endDate) { const interval = getDefaultIntervalByDates(
if (isSameDay(state.startDate, state.endDate)) { state.startDate,
state.interval = 'hour'; state.endDate
} else if (isSameMonth(state.startDate, state.endDate)) { );
state.interval = 'day'; if (interval) {
} else { state.interval = interval;
state.interval = 'month';
}
} }
}, },

View File

@@ -1,9 +1,10 @@
import { isValidElement } from 'react';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import Link from 'next/link'; import Link from 'next/link';
interface KeyValueProps { interface KeyValueProps {
name: string; name: string;
value: string | number | undefined; value: unknown;
onClick?: () => void; onClick?: () => void;
href?: string; href?: string;
} }
@@ -11,6 +12,10 @@ interface KeyValueProps {
export function KeyValue({ href, onClick, name, value }: KeyValueProps) { export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
const clickable = href || onClick; const clickable = href || onClick;
const Component = href ? (Link as any) : onClick ? 'button' : 'div'; const Component = href ? (Link as any) : onClick ? 'button' : 'div';
if (!isValidElement(value)) {
return null;
}
return ( return (
<Component <Component
className={cn( className={cn(
@@ -35,6 +40,10 @@ export function KeyValue({ href, onClick, name, value }: KeyValueProps) {
export function KeyValueSubtle({ href, onClick, name, value }: KeyValueProps) { export function KeyValueSubtle({ href, onClick, name, value }: KeyValueProps) {
const clickable = href || onClick; const clickable = href || onClick;
const Component = href ? (Link as any) : onClick ? 'button' : 'div'; const Component = href ? (Link as any) : onClick ? 'button' : 'div';
if (!isValidElement(value)) {
return null;
}
return ( return (
<Component <Component
className="group flex text-[10px] sm:text-xs gap-2 font-medium self-start min-w-0 max-w-full items-center" className="group flex text-[10px] sm:text-xs gap-2 font-medium self-start min-w-0 max-w-full items-center"

View File

@@ -1,3 +1,5 @@
import { isSameDay, isSameMonth } from 'date-fns';
export const NOT_SET_VALUE = '(not set)'; export const NOT_SET_VALUE = '(not set)';
export const operators = { export const operators = {
@@ -100,3 +102,19 @@ export function getDefaultIntervalByRange(
} }
return 'month'; return 'month';
} }
export function getDefaultIntervalByDates(
startDate: string | null,
endDate: string | null
): null | keyof typeof intervals {
if (startDate && endDate) {
if (isSameDay(startDate, endDate)) {
return 'hour';
} else if (isSameMonth(startDate, endDate)) {
return 'day';
}
return 'month';
}
return null;
}

View File

@@ -11,6 +11,7 @@
"@mixan/eslint-config": "workspace:*", "@mixan/eslint-config": "workspace:*",
"@mixan/prettier-config": "workspace:*", "@mixan/prettier-config": "workspace:*",
"@mixan/tsconfig": "workspace:*", "@mixan/tsconfig": "workspace:*",
"date-fns": "^3.3.1",
"eslint": "^8.48.0", "eslint": "^8.48.0",
"prettier": "^3.0.3", "prettier": "^3.0.3",
"prisma": "^5.1.1", "prisma": "^5.1.1",

View File

@@ -40,7 +40,7 @@ CREATE TABLE openpanel.events_bots (
ORDER BY ORDER BY
(project_id, created_at) SETTINGS index_granularity = 8192; (project_id, created_at) SETTINGS index_granularity = 8192;
CREATE TABLE profiles ( CREATE TABLE openpanel.profiles (
`id` String, `id` String,
`external_id` String, `external_id` String,
`first_name` String, `first_name` String,

4
pnpm-lock.yaml generated
View File

@@ -716,6 +716,9 @@ importers:
'@mixan/tsconfig': '@mixan/tsconfig':
specifier: workspace:* specifier: workspace:*
version: link:../../tooling/typescript version: link:../../tooling/typescript
date-fns:
specifier: ^3.3.1
version: 3.3.1
eslint: eslint:
specifier: ^8.48.0 specifier: ^8.48.0
version: 8.56.0 version: 8.56.0
@@ -7947,7 +7950,6 @@ packages:
/date-fns@3.3.1: /date-fns@3.3.1:
resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==}
dev: false
/dayjs@1.11.10: /dayjs@1.11.10:
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==} resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}