Refactor part 2: Electric Boogaloo. Standardize way of passing filter parameters.

This commit is contained in:
Mike Cao 2025-07-04 01:23:11 -07:00
parent f26f1b0581
commit cdf391d5c2
90 changed files with 867 additions and 709 deletions

View file

@ -3,17 +3,11 @@ import { useUsersQuery } from '@/components/hooks';
import { UsersTable } from './UsersTable';
import { ReactNode } from 'react';
export function UsersDataTable({
showActions,
children,
}: {
showActions?: boolean;
children?: ReactNode;
}) {
export function UsersDataTable({ showActions }: { showActions?: boolean; children?: ReactNode }) {
const queryResult = useUsersQuery();
return (
<DataGrid queryResult={queryResult} renderEmpty={() => children}>
<DataGrid queryResult={queryResult} allowSearch={true}>
{({ data }) => <UsersTable data={data} showActions={showActions} />}
</DataGrid>
);

View file

@ -19,7 +19,6 @@ import { TagsTable } from '@/components/metrics/TagsTable';
import { getCompareDate } from '@/lib/date';
import { formatNumber } from '@/lib/format';
import { useState } from 'react';
import { useWebsites } from '@/store/websites';
import { Panel } from '@/components/common/Panel';
import { DateDisplay } from '@/components/common/DateDisplay';
@ -42,8 +41,7 @@ const views = {
export function WebsiteCompareTables({ websiteId }: { websiteId: string }) {
const [data, setData] = useState([]);
const { dateRange } = useDateRange(websiteId);
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare);
const { dateRange, dateCompare } = useDateRange(websiteId);
const { formatMessage, labels } = useMessages();
const {
updateParams,

View file

@ -17,43 +17,43 @@ export function WebsiteMetricsBar({
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
const isAllTime = dateRange.value === 'all';
const { pageviews, visitors, visits, bounces, totaltime, previous } = data || {};
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
const metrics = data
? [
{
value: visitors,
label: formatMessage(labels.visitors),
change: visitors - previous.visitors,
change: visitors - comparison.visitors,
formatValue: formatLongNumber,
},
{
value: visits,
label: formatMessage(labels.visits),
change: visits - previous.visits,
change: visits - comparison.visits,
formatValue: formatLongNumber,
},
{
value: pageviews,
label: formatMessage(labels.views),
change: pageviews - previous.pageviews,
change: pageviews - comparison.pageviews,
formatValue: formatLongNumber,
},
{
label: formatMessage(labels.bounceRate),
value: (Math.min(visits, bounces) / visits) * 100,
prev: (Math.min(previous.visits, previous.bounces) / previous.visits) * 100,
prev: (Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100,
change:
(Math.min(visits, bounces) / visits) * 100 -
(Math.min(previous.visits, previous.bounces) / previous.visits) * 100,
(Math.min(comparison.visits, comparison.bounces) / comparison.visits) * 100,
formatValue: n => Math.round(+n) + '%',
reverseColors: true,
},
{
label: formatMessage(labels.visitDuration),
value: totaltime / visits,
prev: previous.totaltime / previous.visits,
change: totaltime / visits - previous.totaltime / previous.visits,
prev: comparison.totaltime / comparison.visits,
change: totaltime / visits - comparison.totaltime / comparison.visits,
formatValue: n =>
`${+n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`,
},

View file

@ -1,7 +1,9 @@
import { useWebsiteEventsQuery } from '@/components/hooks';
import { useState } from 'react';
import { useMessages, useWebsiteEventsQuery } from '@/components/hooks';
import { EventsTable } from './EventsTable';
import { DataGrid } from '@/components/common/DataGrid';
import { ReactNode } from 'react';
import { FilterButtons } from '@/components/common/FilterButtons';
export function EventsDataTable({
websiteId,
@ -10,10 +12,37 @@ export function EventsDataTable({
teamId?: string;
children?: ReactNode;
}) {
const { formatMessage, labels } = useMessages();
const queryResult = useWebsiteEventsQuery(websiteId);
const [view, setView] = useState('all');
const buttons = [
{
id: 'all',
label: formatMessage(labels.all),
},
{
id: 'page',
label: formatMessage(labels.page),
},
{
id: 'event',
label: formatMessage(labels.event),
},
];
const renderActions = () => {
return <FilterButtons items={buttons} value={view} onChange={setView} />;
};
return (
<DataGrid queryResult={queryResult} allowSearch={true} autoFocus={false}>
<DataGrid
queryResult={queryResult}
allowSearch={true}
autoFocus={false}
allowPaging={true}
renderActions={renderActions}
>
{({ data }) => <EventsTable data={data} />}
</DataGrid>
);

View file

@ -5,6 +5,7 @@ import { Avatar } from '@/components/common/Avatar';
import Link from 'next/link';
import { Bolt, Eye } from '@/components/icons';
import { DateDistance } from '@/components/common/DateDistance';
import { TypeIcon } from '@/components/common/TypeIcon';
export function EventsTable({ data = [] }) {
const { formatMessage, labels } = useMessages();
@ -16,13 +17,6 @@ export function EventsTable({ data = [] }) {
return (
<DataTable data={data}>
<DataColumn id="session" label={formatMessage(labels.session)} width="100px">
{(row: any) => (
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={64} />
</Link>
)}
</DataColumn>
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
{(row: any) => {
return (
@ -34,8 +28,21 @@ export function EventsTable({ data = [] }) {
);
}}
</DataColumn>
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
<DataColumn id="created" width="1fr" align="end">
{(row: any) => (
<Row alignItems="center" gap>
<DateDistance date={new Date(row.createdAt)} />
<Link href={renderUrl(`/websites/${row.websiteId}/sessions/${row.sessionId}`)}>
<Avatar seed={row.sessionId} size={32} />
</Link>
<Row alignItems="center" gap="1">
<TypeIcon type="country" value={row.country} />
<TypeIcon type="browser" value={row.browser} />
<TypeIcon type="os" value={row.os} />
<TypeIcon type="device" value={row.device} />
</Row>
</Row>
)}
</DataColumn>
</DataTable>
);

View file

@ -30,15 +30,11 @@ export function Attribution({
}: AttributionProps) {
const { data, error, isLoading } = useResultQuery<any>('attribution', {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters: {
model,
type,
step,
},
startDate,
endDate,
model,
type,
step,
});
const { formatMessage, labels } = useMessages();

View file

@ -7,12 +7,10 @@ export interface BreakdownProps {
websiteId: string;
startDate: Date;
endDate: Date;
parameters: {
fields: string[];
};
selectedFields: string[];
}
export function Breakdown({ websiteId, parameters, startDate, endDate }: BreakdownProps) {
export function Breakdown({ websiteId, selectedFields = [], startDate, endDate }: BreakdownProps) {
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const { fields } = useFields();
@ -20,19 +18,17 @@ export function Breakdown({ websiteId, parameters, startDate, endDate }: Breakdo
'breakdown',
{
websiteId,
dateRange: {
startDate,
endDate,
},
parameters,
startDate,
endDate,
fields: selectedFields,
},
{ enabled: !!parameters.fields.length },
{ enabled: !!selectedFields.length },
);
return (
<LoadingPanel data={data} isLoading={isLoading} error={error}>
<DataTable data={data}>
{parameters?.fields.map(field => {
{selectedFields.map(field => {
return (
<DataColumn key={field} id={field} label={fields.find(f => f.name === field)?.label}>
{row => {

View file

@ -23,7 +23,7 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
websiteId={websiteId}
startDate={startDate}
endDate={endDate}
parameters={{ fields }}
selectedFields={fields}
/>
</Panel>
</Column>

View file

@ -17,15 +17,11 @@ type FunnelResult = {
remaining: number;
};
export function Funnel({ id, name, type, parameters, websiteId, startDate, endDate }) {
export function Funnel({ id, name, type, parameters, websiteId }) {
const { formatMessage, labels } = useMessages();
const { data, error, isLoading } = useResultQuery<any>(type, {
const { data, error, isLoading } = useResultQuery(type, {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters,
...parameters,
});
return (

View file

@ -9,7 +9,7 @@ import { LoadingPanel } from '@/components/common/LoadingPanel';
import { Panel } from '@/components/common/Panel';
export function FunnelsPage({ websiteId }: { websiteId: string }) {
const { result, query } = useReportsQuery({ websiteId, type: 'funnel' });
const { data, isLoading, error } = useReportsQuery({ websiteId, type: 'funnel' });
const {
dateRange: { startDate, endDate },
} = useDateRange(websiteId);
@ -20,14 +20,16 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
<SectionHeader>
<FunnelAddButton websiteId={websiteId} />
</SectionHeader>
<LoadingPanel data={result?.data} isLoading={query?.isLoading} error={query?.error}>
<Grid gap>
{result?.data?.map((report: any) => (
<Panel key={report.id}>
<Funnel {...report} startDate={startDate} endDate={endDate} />
</Panel>
))}
</Grid>
<LoadingPanel data={data} isLoading={isLoading} error={error}>
{data && (
<Grid gap>
{data['data']?.map((report: any) => (
<Panel key={report.id}>
<Funnel {...report} startDate={startDate} endDate={endDate} />
</Panel>
))}
</Grid>
)}
</LoadingPanel>
</Column>
);

View file

@ -26,11 +26,9 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
const { formatMessage, labels } = useMessages();
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters,
startDate,
endDate,
...parameters,
});
const isPage = parameters?.type === 'page';

View file

@ -22,28 +22,15 @@ export interface JourneyProps {
endStep?: string;
}
export function Journey({
websiteId,
startDate,
endDate,
steps,
startStep,
endStep,
}: JourneyProps) {
export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps) {
const [selectedNode, setSelectedNode] = useState(null);
const [activeNode, setActiveNode] = useState(null);
const { formatMessage, labels } = useMessages();
const { data, error, isLoading } = useResultQuery<any>('journey', {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters: {
steps,
startStep,
endStep,
},
steps,
startStep,
endStep,
});
useEscapeKey(() => setSelectedNode(null));

View file

@ -1,7 +1,7 @@
import { ReactNode } from 'react';
import { Grid, Row, Column, Text, Icon } from '@umami/react-zen';
import { Users } from '@/components/icons';
import { useMessages, useLocale, useResultQuery, useTimezone } from '@/components/hooks';
import { useMessages, useLocale, useResultQuery } from '@/components/hooks';
import { formatDate } from '@/lib/date';
import { formatLongNumber } from '@/lib/format';
import { Panel } from '@/components/common/Panel';
@ -19,14 +19,10 @@ export interface RetentionProps {
export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) {
const { formatMessage, labels } = useMessages();
const { locale } = useLocale();
const { timezone } = useTimezone();
const { data, error, isLoading } = useResultQuery<any>('retention', {
const { data, error, isLoading } = useResultQuery('retention', {
websiteId,
dateRange: {
startDate,
endDate,
timezone,
},
startDate,
endDate,
});
const rows =

View file

@ -8,7 +8,7 @@ import { endOfMonth, startOfMonth } from 'date-fns';
export function RetentionPage({ websiteId }: { websiteId: string }) {
const {
dateRange: { startDate },
} = useDateRange(websiteId);
} = useDateRange(websiteId, { ignoreOffset: true });
const monthStartDate = startOfMonth(startDate);
const monthEndDate = endOfMonth(startDate);

View file

@ -32,13 +32,9 @@ export function Revenue({ websiteId, startDate, endDate }: RevenueProps) {
const unit = getMinimumUnit(startDate, endDate);
const { data, error, isLoading } = useResultQuery<any>('revenue', {
websiteId,
dateRange: {
startDate,
endDate,
},
parameters: {
currency,
},
startDate,
endDate,
currency,
});
const renderCountryName = useCallback(

View file

@ -18,10 +18,8 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) {
const { formatMessage, labels } = useMessages();
const { data, error, isLoading } = useResultQuery<any>('utm', {
websiteId,
dateRange: {
startDate,
endDate,
},
startDate,
endDate,
});
return (

View file

@ -1,21 +1,15 @@
import { useWebsiteSessionsQuery } from '@/components/hooks';
import { SessionsTable } from './SessionsTable';
import { DataGrid } from '@/components/common/DataGrid';
import { ReactNode } from 'react';
export function SessionsDataTable({
websiteId,
children,
}: {
websiteId?: string;
teamId?: string;
children?: ReactNode;
}) {
export function SessionsDataTable({ websiteId }: { websiteId?: string; teamId?: string }) {
const queryResult = useWebsiteSessionsQuery(websiteId);
return (
<DataGrid queryResult={queryResult} renderEmpty={() => children} allowPaging>
{({ data }) => <SessionsTable data={data} showDomain={!websiteId} />}
<DataGrid queryResult={queryResult} allowPaging allowSearch>
{({ data }) => {
return <SessionsTable data={data} showDomain={!websiteId} />;
}}
</DataGrid>
);
}

View file

@ -14,7 +14,7 @@ export function SessionsTable({ data = [] }: { data: any[]; showDomain?: boolean
<DataColumn id="id" label={formatMessage(labels.session)} width="100px">
{(row: any) => (
<Link href={`sessions/${row.id}`}>
<Avatar seed={row.id} size={64} />
<Avatar seed={row.id} size={48} />
</Link>
)}
</DataColumn>