From ea83afbc13563662955b3c5e67ae67d71c31fc9b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sun, 29 Jun 2025 15:36:43 -0700 Subject: [PATCH] Fixed retention report showing wrong dates. Changed Breakdown field select to modal. --- .../[websiteId]/WebsiteCompareTables.tsx | 2 +- .../websites/[websiteId]/WebsiteControls.tsx | 2 +- .../[websiteId]/WebsiteMetricsBar.tsx | 12 +- .../reports/breakdown/Breakdown.tsx | 8 +- .../reports/breakdown/BreakdownPage.tsx | 86 ++++---------- .../reports/breakdown/FieldSelectForm.tsx | 46 ++++++++ .../reports/goals/GoalEditForm.tsx | 2 +- .../reports/retention/Retention.tsx | 2 +- src/app/api/reports/breakdown/route.ts | 2 + src/app/api/reports/retention/route.ts | 3 +- src/components/common/Breadcrumb.module.css | 10 -- src/components/common/Breadcrumb.tsx | 37 ------ src/components/common/DataGrid.module.css | 34 ------ src/components/common/LinkButton.module.css | 107 ------------------ .../hooks/queries/useResultQuery.ts | 17 ++- .../hooks/queries/useWebsiteMetricsQuery.ts | 6 +- src/components/icons.ts | 2 + src/lib/schema.ts | 3 +- src/lib/types.ts | 2 +- src/queries/sql/reports/getRetention.ts | 2 +- 20 files changed, 108 insertions(+), 277 deletions(-) create mode 100644 src/app/(main)/websites/[websiteId]/reports/breakdown/FieldSelectForm.tsx delete mode 100644 src/components/common/Breadcrumb.module.css delete mode 100644 src/components/common/Breadcrumb.tsx delete mode 100644 src/components/common/DataGrid.module.css delete mode 100644 src/components/common/LinkButton.module.css diff --git a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx b/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx index 7af903ef..c9487ba6 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteCompareTables.tsx @@ -24,7 +24,7 @@ import { Panel } from '@/components/common/Panel'; import { DateDisplay } from '@/components/common/DateDisplay'; const views = { - url: PagesTable, + path: PagesTable, title: PagesTable, referrer: ReferrersTable, browser: BrowsersTable, diff --git a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx index 1e36b729..97fabf2a 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteControls.tsx @@ -20,7 +20,7 @@ export function WebsiteControls({ return ( - {allowFilter && } + {allowFilter ? :
} {allowDateFilter && } {allowMonthFilter && } diff --git a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx index d147b9b3..246fa576 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteMetricsBar.tsx @@ -30,9 +30,9 @@ export function WebsiteMetricsBar({ const metrics = data ? [ { - value: pageviews, - label: formatMessage(labels.views), - change: pageviews - previous.pageviews, + value: visitors, + label: formatMessage(labels.visitors), + change: visitors - previous.visitors, formatValue: formatLongNumber, }, { @@ -42,9 +42,9 @@ export function WebsiteMetricsBar({ formatValue: formatLongNumber, }, { - value: visitors, - label: formatMessage(labels.visitors), - change: visitors - previous.visitors, + value: pageviews, + label: formatMessage(labels.views), + change: pageviews - previous.pageviews, formatValue: formatLongNumber, }, { diff --git a/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx b/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx index 053c6bb1..bc5e2111 100644 --- a/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/breakdown/Breakdown.tsx @@ -46,14 +46,14 @@ export function Breakdown({ websiteId, parameters, startDate, endDate }: Breakdo ); })} - - {row => row?.['views']?.toLocaleString()} + + {row => row?.['visitors']?.toLocaleString()} {row => row?.['visits']?.toLocaleString()} - - {row => row?.['visitors']?.toLocaleString()} + + {row => row?.['views']?.toLocaleString()} {row => { diff --git a/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx b/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx index acf68948..890c440b 100644 --- a/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/breakdown/BreakdownPage.tsx @@ -1,22 +1,12 @@ 'use client'; import { useState } from 'react'; -import { - List, - ListItem, - Button, - Column, - Box, - Grid, - Text, - Icon, - Popover, - DialogTrigger, -} from '@umami/react-zen'; -import { useDateRange, useMessages, useFields } from '@/components/hooks'; -import { SquarePlus, Chevron } from '@/components/icons'; +import { Button, Column, Box, Text, Icon, DialogTrigger, Modal, Dialog } from '@umami/react-zen'; +import { useDateRange, useMessages } from '@/components/hooks'; +import { ListCheck } from '@/components/icons'; import { Panel } from '@/components/common/Panel'; import { Breakdown } from './Breakdown'; import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { FieldSelectForm } from '@/app/(main)/websites/[websiteId]/reports/breakdown/FieldSelectForm'; export function BreakdownPage({ websiteId }: { websiteId: string }) { const { @@ -27,9 +17,7 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) { return ( - - - + { - const [selected, setSelected] = useState(value); - const [isOpen, setIsOpen] = useState(false); const { formatMessage, labels } = useMessages(); - const { fields } = useFields(); - - const handleChange = value => { - setSelected(value); - }; - - const handleApply = () => { - setIsOpen(false); - onChange?.(selected); - }; - - const handleClose = () => { - setIsOpen(false); - setSelected(value); - }; return ( - - - - - - {fields.map(({ name, label }) => { - return ( - - {label} - - ); - })} - - - - - - - - + + + + + + {({ close }) => ( + + )} + + + + ); }; diff --git a/src/app/(main)/websites/[websiteId]/reports/breakdown/FieldSelectForm.tsx b/src/app/(main)/websites/[websiteId]/reports/breakdown/FieldSelectForm.tsx new file mode 100644 index 00000000..9d828c7d --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/reports/breakdown/FieldSelectForm.tsx @@ -0,0 +1,46 @@ +import { Column, List, ListItem, Grid, Button } from '@umami/react-zen'; +import { useFields, useMessages } from '@/components/hooks'; +import { useState } from 'react'; + +export function FieldSelectForm({ + selectedFields = [], + onChange, + onClose, +}: { + selectedFields?: string[]; + onChange: (values: string[]) => void; + onClose?: () => void; +}) { + const [selected, setSelected] = useState(selectedFields); + const { formatMessage, labels } = useMessages(); + const { fields } = useFields(); + + const handleChange = (value: string[]) => { + setSelected(value); + }; + + const handleApply = () => { + onChange?.(selected); + onClose(); + }; + + return ( + + + {fields.map(({ name, label }) => { + return ( + + {label} + + ); + })} + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx b/src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx index b42545de..e1a3d97a 100644 --- a/src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx +++ b/src/app/(main)/websites/[websiteId]/reports/goals/GoalEditForm.tsx @@ -69,7 +69,7 @@ export function GoalEditForm({ label={formatMessage(labels.name)} rules={{ required: formatMessage(labels.required) }} > - + ('retention', { websiteId, - timezone, dateRange: { startDate, endDate, + timezone, }, }); diff --git a/src/app/api/reports/breakdown/route.ts b/src/app/api/reports/breakdown/route.ts index 55a07369..de822f63 100644 --- a/src/app/api/reports/breakdown/route.ts +++ b/src/app/api/reports/breakdown/route.ts @@ -15,6 +15,7 @@ export async function POST(request: Request) { websiteId, dateRange: { startDate, endDate }, parameters: { fields }, + filters, } = body; if (!(await canViewWebsite(auth, websiteId))) { @@ -24,6 +25,7 @@ export async function POST(request: Request) { const data = await getBreakdown(websiteId, fields, { startDate: new Date(startDate), endDate: new Date(endDate), + ...filters, }); return json(data); diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts index cc7433aa..76e54e9f 100644 --- a/src/app/api/reports/retention/route.ts +++ b/src/app/api/reports/retention/route.ts @@ -13,8 +13,7 @@ export async function POST(request: Request) { const { websiteId, - dateRange: { startDate, endDate }, - timezone, + dateRange: { startDate, endDate, timezone }, } = body; if (!(await canViewWebsite(auth, websiteId))) { diff --git a/src/components/common/Breadcrumb.module.css b/src/components/common/Breadcrumb.module.css deleted file mode 100644 index 81e7524f..00000000 --- a/src/components/common/Breadcrumb.module.css +++ /dev/null @@ -1,10 +0,0 @@ -.bar { - font-size: 12px; - font-weight: 700; - text-transform: uppercase; - color: var(--base600); -} - -.link span { - color: var(--base700) !important; -} diff --git a/src/components/common/Breadcrumb.tsx b/src/components/common/Breadcrumb.tsx deleted file mode 100644 index 163d33d9..00000000 --- a/src/components/common/Breadcrumb.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Fragment } from 'react'; -import Link from 'next/link'; -import { Row, Icon, Text } from '@umami/react-zen'; -import { Chevron } from '@/components/icons'; -import styles from './Breadcrumb.module.css'; - -export interface BreadcrumbProps { - data: { - url?: string; - label: string; - }[]; -} - -export function Breadcrumb({ data }: BreadcrumbProps) { - return ( - - {data.map((a, i) => { - return ( - - {a.url ? ( - - {a.label} - - ) : ( - {a.label} - )} - {i !== data.length - 1 ? ( - - - - ) : null} - - ); - })} - - ); -} diff --git a/src/components/common/DataGrid.module.css b/src/components/common/DataGrid.module.css deleted file mode 100644 index 9a7cffb7..00000000 --- a/src/components/common/DataGrid.module.css +++ /dev/null @@ -1,34 +0,0 @@ -.search { - max-width: 300px; - margin: 20px 0; -} - -.body { - display: flex; - flex-direction: column; - position: relative; - overflow-x: auto; -} - -.body td { - display: flex; - gap: 10px; - min-height: 70px; - align-items: center; -} - -.body > div > div > div { - display: flex; - gap: 10px; -} - -.pager { - margin: 20px 0; -} - -.status { - display: flex; - align-items: center; - justify-content: center; - min-height: 200px; -} diff --git a/src/components/common/LinkButton.module.css b/src/components/common/LinkButton.module.css deleted file mode 100644 index bb23aeba..00000000 --- a/src/components/common/LinkButton.module.css +++ /dev/null @@ -1,107 +0,0 @@ -.button { - display: flex; - align-items: center; - align-self: flex-start; - white-space: nowrap; - gap: var(--size200); - font-family: inherit; - color: var(--base900); - background: var(--base100); - border: 1px solid transparent; - border-radius: var(--border-radius); - min-height: var(--base-height); - padding: 0 var(--size600); - position: relative; - cursor: pointer; -} - -.button:hover { - background: var(--base200); -} - -.button:active { - background: var(--base300); -} - -.button:visited { - color: var(--base900); -} - -.button.disabled { - color: var(--disabled-color) !important; - background-color: var(--disabled-background) !important; - border-color: transparent !important; - pointer-events: none; -} - -.button.primary { - color: var(--light50); - background: var(--primary-color); -} - -.button.primary:hover { - color: var(--light50); - background: var(--primary500); -} - -.button.primary:active { - color: var(--light50); - background: var(--primary600); -} - -.button.secondary { - border: 1px solid var(--border-color); - background: var(--base50); -} - -.button.secondary:hover { - background: var(--base75); -} - -.button.secondary:active { - background: var(--base100); -} - -.button.quiet { - color: var(--base900); - background: transparent; -} - -.button.quiet:hover { - background: var(--base100); -} - -.button.quiet:active { - background: var(--base200); -} - -.button.danger { - color: var(--light50); - background: var(--red800); -} - -.button.danger:hover { - color: var(--light50); - background: var(--red900); -} - -.button.danger:active { - color: var(--light50); - background: var(--red1000); -} - -.button.size-sm { - font-size: var(--font-size-sm); - height: calc(var(--base-height) * 0.75); - padding: 0 calc(var(--size600) * 0.75); -} - -.button.size-md { - font-size: var(--font-size-md); -} - -.button.size-lg { - font-size: var(--font-size-lg); - height: calc(var(--base-height) * 1.25); - padding: 0 calc(var(--size600) * 1.25); -} diff --git a/src/components/hooks/queries/useResultQuery.ts b/src/components/hooks/queries/useResultQuery.ts index be84193d..f4e3d5e7 100644 --- a/src/components/hooks/queries/useResultQuery.ts +++ b/src/components/hooks/queries/useResultQuery.ts @@ -1,4 +1,5 @@ -import { useApi } from '@/components/hooks'; +import { useApi } from '../useApi'; +import { useFilterParams } from '../useFilterParams'; import { ReactQueryOptions } from '@/lib/types'; export function useResultQuery( @@ -6,11 +7,21 @@ export function useResultQuery( params?: { [key: string]: any }, options?: ReactQueryOptions, ) { + const { websiteId } = params; const { post, useQuery } = useApi(); + const filterParams = useFilterParams(websiteId); return useQuery({ - queryKey: ['reports', type, params], - queryFn: () => post(`/reports/${type}`, { type, ...params }), + queryKey: [ + 'reports', + { + type, + websiteId, + ...filterParams, + ...params, + }, + ], + queryFn: () => post(`/reports/${type}`, { type, ...filterParams, ...params }), enabled: !!type, ...options, }); diff --git a/src/components/hooks/queries/useWebsiteMetricsQuery.ts b/src/components/hooks/queries/useWebsiteMetricsQuery.ts index 89adf818..5952de44 100644 --- a/src/components/hooks/queries/useWebsiteMetricsQuery.ts +++ b/src/components/hooks/queries/useWebsiteMetricsQuery.ts @@ -11,7 +11,7 @@ export type WebsiteMetricsData = { export function useWebsiteMetricsQuery( websiteId: string, - queryParams: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number }, + params: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number }, options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); @@ -24,14 +24,14 @@ export function useWebsiteMetricsQuery( { websiteId, ...filterParams, - ...queryParams, + ...params, }, ], queryFn: async () => get(`/websites/${websiteId}/metrics`, { ...filterParams, [searchParams.get('view')]: undefined, - ...queryParams, + ...params, }), enabled: !!websiteId, placeholderData: keepPreviousData, diff --git a/src/components/icons.ts b/src/components/icons.ts index d57d36a9..c566e4b3 100644 --- a/src/components/icons.ts +++ b/src/components/icons.ts @@ -10,6 +10,7 @@ export { Download, Edit, Ellipsis, + Equal, Eye, ExternalLink, File, @@ -20,6 +21,7 @@ export { KeyRound, LayoutDashboard, Link, + ListCheck, ListFilter, LockKeyhole, LogOut, diff --git a/src/lib/schema.ts b/src/lib/schema.ts index fd2e3f8d..fb51c8e4 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -88,7 +88,8 @@ export const reportParms = { endDate: z.coerce.date(), num: z.coerce.number().optional(), offset: z.coerce.number().optional(), - unit: z.string().optional(), + timezone: timezoneParam.optional(), + unit: unitParam.optional(), value: z.string().optional(), }), }; diff --git a/src/lib/types.ts b/src/lib/types.ts index 3373be34..f6f2c8a1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -68,7 +68,7 @@ export interface QueryFilters { timezone?: string; unit?: string; eventType?: number; - url?: string; + path?: string; referrer?: string; title?: string; query?: string; diff --git a/src/queries/sql/reports/getRetention.ts b/src/queries/sql/reports/getRetention.ts index 924930db..5e871500 100644 --- a/src/queries/sql/reports/getRetention.ts +++ b/src/queries/sql/reports/getRetention.ts @@ -108,7 +108,7 @@ async function clickhouseQuery( user_activities AS ( select distinct w.session_id, - (${getDateSQL('created_at', unit)} - c.cohort_date) / 86400 as day_number + (${getDateSQL('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number from website_event w join cohort_items c on w.session_id = c.session_id