Updated reports.

This commit is contained in:
Mike Cao 2025-06-08 22:21:28 -07:00
parent 28e872f219
commit 01bd21c5b4
75 changed files with 1373 additions and 980 deletions

View file

@ -1,12 +0,0 @@
.container {
color: var(--base500);
font-size: var(--font-size-md);
position: relative;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
width: 100%;
height: 100%;
min-height: 70px;
}

View file

@ -1,18 +1,16 @@
import classNames from 'classnames';
import { Row } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
import styles from './Empty.module.css';
export interface EmptyProps {
message?: string;
className?: string;
}
export function Empty({ message, className }: EmptyProps) {
export function Empty({ message }: EmptyProps) {
const { formatMessage, messages } = useMessages();
return (
<div className={classNames(styles.container, className)}>
<Row color="muted" alignItems="center" justifyContent="center" width="100%" height="100%">
{message || formatMessage(messages.noDataAvailable)}
</div>
</Row>
);
}

View file

@ -1,19 +1,28 @@
import { ReactNode } from 'react';
import { Icon, Text, Column } from '@umami/react-zen';
import { Logo } from '@/components/icons';
export interface EmptyPlaceholderProps {
message?: string;
title?: string;
description?: string;
icon?: ReactNode;
children?: ReactNode;
}
export function EmptyPlaceholder({ message, icon, children }: EmptyPlaceholderProps) {
export function EmptyPlaceholder({ title, description, icon, children }: EmptyPlaceholderProps) {
return (
<Column alignItems="center" justifyContent="center" gap="5" height="100%" width="100%">
<Icon size="xl">{icon || <Logo />}</Icon>
<Text>{message}</Text>
<div>{children}</div>
{icon && (
<Icon color="10" size="xl">
{icon}
</Icon>
)}
{title && (
<Text weight="bold" size="4">
{title}
</Text>
)}
{description && <Text color="muted">{description}</Text>}
{children}
</Column>
);
}

View file

@ -1,7 +1,8 @@
import { useState, Key } from 'react';
import { Grid, Row, Column, Label, List, ListItem, Button, Heading, Text } from '@umami/react-zen';
import { Grid, Row, Column, Label, List, ListItem, Button, Heading } from '@umami/react-zen';
import { useFilters, useMessages } from '@/components/hooks';
import { FilterRecord } from '@/components/common/FilterRecord';
import { Empty } from '@/components/common/Empty';
export interface FilterEditFormProps {
websiteId?: string;
@ -11,7 +12,7 @@ export interface FilterEditFormProps {
}
export function FilterEditForm({ data = [], onChange, onClose }: FilterEditFormProps) {
const { formatMessage, labels } = useMessages();
const { formatMessage, labels, messages } = useMessages();
const [filters, setFilters] = useState(data);
const { fields } = useFilters();
@ -72,7 +73,7 @@ export function FilterEditForm({ data = [], onChange, onClose }: FilterEditFormP
/>
);
})}
{!filters.length && <Text align="center">{formatMessage(labels.none)}</Text>}
{!filters.length && <Empty message={formatMessage(messages.nothingSelected)} />}
</Column>
<Row alignItems="center" justifyContent="flex-end" gridColumn="span 2" gap>
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>

View file

@ -1,16 +0,0 @@
.panel {
display: flex;
flex-direction: column;
position: relative;
flex: 1;
height: 100%;
}
.loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}

View file

@ -1,9 +1,7 @@
import { ReactNode } from 'react';
import classNames from 'classnames';
import { Spinner, Dots } from '@umami/react-zen';
import { Spinner, Dots, Column, type ColumnProps } from '@umami/react-zen';
import { ErrorMessage } from '@/components/common/ErrorMessage';
import { Empty } from '@/components/common/Empty';
import styles from './LoadingPanel.module.css';
export function LoadingPanel({
error,
@ -12,25 +10,23 @@ export function LoadingPanel({
isLoading,
loadingIcon = 'dots',
renderEmpty = () => <Empty />,
className,
children,
...props
}: {
data?: any;
error?: Error;
isEmpty?: boolean;
isFetched?: boolean;
isLoading?: boolean;
loadingIcon?: 'dots' | 'spinner';
renderEmpty?: () => ReactNode;
className?: string;
children: ReactNode;
}) {
} & ColumnProps) {
return (
<div className={classNames(styles.panel, className)}>
<Column {...props}>
{isLoading && !isFetched && (loadingIcon === 'dots' ? <Dots /> : <Spinner />)}
{error && <ErrorMessage />}
{!error && !isLoading && isEmpty && renderEmpty()}
{!error && !isLoading && !isEmpty && children}
</div>
</Column>
);
}

View file

@ -4,7 +4,7 @@ import { AlertBanner, Loading, Column } from '@umami/react-zen';
import { useMessages } from '@/components/hooks';
export function PageBody({
maxWidth = '1600px',
maxWidth = '1320px',
error,
isLoading,
children,

View file

@ -5,11 +5,13 @@ export function PageHeader({
title,
description,
icon,
showBorder = true,
children,
}: {
title: string;
description?: string;
icon?: ReactNode;
showBorder?: boolean;
allowEdit?: boolean;
className?: string;
children?: ReactNode;
@ -19,7 +21,7 @@ export function PageHeader({
justifyContent="space-between"
alignItems="center"
paddingY="6"
border="bottom"
border={showBorder ? 'bottom' : undefined}
width="100%"
>
<Row alignItems="center" gap="3">

View file

@ -3,14 +3,11 @@ export * from './queries/useActiveUsersQuery';
export * from './queries/useEventDataEventsQuery';
export * from './queries/useEventDataPropertiesQuery';
export * from './queries/useEventDataValuesQuery';
export * from './queries/useGoalsQuery';
export * from './queries/useLoginQuery';
export * from './queries/useRealtimeQuery';
export * from './queries/useResultQuery';
export * from './queries/useReportQuery';
export * from './queries/useReportsQuery';
export * from './queries/useRetentionQuery';
export * from './queries/useRevenueQuery';
export * from './queries/useSessionActivityQuery';
export * from './queries/useSessionDataQuery';
export * from './queries/useSessionDataPropertiesQuery';

View file

@ -1,18 +0,0 @@
import { useApi } from '../useApi';
import { usePagedQuery } from '../usePagedQuery';
export function useGoalQuery(
{ websiteId, reportId }: { websiteId: string; reportId: string },
params?: { [key: string]: string | number },
) {
const { post } = useApi();
return usePagedQuery({
queryKey: ['goal', { websiteId, reportId, ...params }],
queryFn: () => {
return post(`/reports/goals`, {
...params,
});
},
});
}

View file

@ -1,20 +0,0 @@
import { useApi } from '../useApi';
import { usePagedQuery } from '../usePagedQuery';
import { useModified } from '../useModified';
export function useGoalsQuery(
{ websiteId }: { websiteId: string },
params?: { [key: string]: string | number },
) {
const { get } = useApi();
const { modified } = useModified(`goals`);
return usePagedQuery({
queryKey: ['goals', { websiteId, modified, ...params }],
queryFn: () => {
return get(`/websites/${websiteId}/goals`, {
...params,
});
},
});
}

View file

@ -1,20 +0,0 @@
import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams';
import { UseQueryOptions } from '@tanstack/react-query';
export function useRetentionQuery(
websiteId: string,
queryParams?: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number },
options?: Omit<UseQueryOptions & { onDataLoad?: (data: any) => void }, 'queryKey' | 'queryFn'>,
) {
const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId);
return useQuery({
queryKey: ['retention', websiteId, { ...filterParams, ...queryParams }],
queryFn: () =>
get(`/websites/${websiteId}/retention`, { websiteId, ...filterParams, ...queryParams }),
enabled: !!websiteId,
...options,
});
}

View file

@ -1,39 +0,0 @@
import { useApi } from '../useApi';
import { useFilterParams } from '../useFilterParams';
import { UseQueryOptions } from '@tanstack/react-query';
export interface RevenueData {
chart: any[];
country: any[];
total: {
sum: number;
count: number;
unique_count: number;
};
table: any[];
}
export function useRevenueQuery(
websiteId: string,
queryParams?: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number },
options?: Omit<
UseQueryOptions<RevenueData, Error, RevenueData, any[]> & { onDataLoad?: (data: any) => void },
'queryKey' | 'queryFn'
>,
) {
const { get, useQuery } = useApi();
const filterParams = useFilterParams(websiteId);
const currency = 'USD';
return useQuery<RevenueData, Error, RevenueData, any[]>({
queryKey: ['revenue', websiteId, { ...filterParams, ...queryParams }],
queryFn: () =>
get(`/websites/${websiteId}/revenue`, {
currency,
...filterParams,
...queryParams,
}),
enabled: !!websiteId,
...options,
});
}

View file

@ -1,16 +0,0 @@
import { useApi } from '../useApi';
export function useRevenueValuesQuery(websiteId: string, startDate: Date, endDate: Date) {
const { get, useQuery } = useApi();
return useQuery({
queryKey: ['revenue:values', { websiteId, startDate, endDate }],
queryFn: () =>
get(`/reports/revenue`, {
websiteId,
startDate,
endDate,
}),
enabled: !!(websiteId && startDate && endDate),
});
}

View file

@ -2,6 +2,7 @@ export {
AlertTriangle as Alert,
ArrowRight as Arrow,
Calendar,
ChartPie,
ChevronRight as Chevron,
Clock,
X as Close,

View file

@ -132,6 +132,7 @@ export const labels = defineMessages({
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
selectRole: { id: 'label.select-role', defaultMessage: 'Select role' },
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
selectFilter: { id: 'label.select-filter', defaultMessage: 'Select filter' },
all: { id: 'label.all', defaultMessage: 'All' },
session: { id: 'label.session', defaultMessage: 'Session' },
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
@ -331,6 +332,7 @@ export const messages = defineMessages({
noUsers: { id: 'message.no-users', defaultMessage: 'There are no users.' },
userDeleted: { id: 'message.user-deleted', defaultMessage: 'User deleted.' },
noDataAvailable: { id: 'message.no-data-available', defaultMessage: 'No data available.' },
nothingSelected: { id: 'message.nothing-selected', defaultMessage: 'Nothing selected.' },
confirmReset: {
id: 'message.confirm-reset',
defaultMessage: 'Are you sure you want to reset {target}?',