Updated reports components.

This commit is contained in:
Mike Cao 2025-03-26 21:54:23 -07:00
parent f5bc3dc6c2
commit 0f6cdf8b80
95 changed files with 580 additions and 698 deletions

View file

@ -1,9 +1,7 @@
'use client';
import { Column } from '@umami/react-zen';
import { Panel } from '@/components/layout/Panel';
import { FilterTags } from '@/components/metrics/FilterTags';
import { Panel } from '@/components/common/Panel';
import { useNavigation } from '@/components/hooks';
import { FILTER_COLUMNS } from '@/lib/constants';
import { WebsiteChart } from './WebsiteChart';
import { WebsiteExpandedView } from './WebsiteExpandedView';
import { WebsiteHeader } from './WebsiteHeader';
@ -11,21 +9,16 @@ import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { WebsiteTableView } from './WebsiteTableView';
export function WebsiteDetailsPage({ websiteId }: { websiteId: string }) {
const { query } = useNavigation();
const { view } = query;
const params = Object.keys(query).reduce((obj, key) => {
if (FILTER_COLUMNS[key]) {
obj[key] = query[key];
}
return obj;
}, {});
const {
query: { view },
} = useNavigation();
return (
<Column gap="3">
<WebsiteHeader websiteId={websiteId} />
<FilterTags websiteId={websiteId} params={params} />
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
<Panel>
<WebsiteMetricsBar websiteId={websiteId} showFilter={true} showChange={true} />
</Panel>
<Panel>
<WebsiteChart websiteId={websiteId} />
</Panel>

View file

@ -1,7 +1,7 @@
import { Icon, Icons, Text, Grid, Column } from '@umami/react-zen';
import { LinkButton } from '@/components/common/LinkButton';
import { useMessages, useNavigation } from '@/components/hooks';
import { SideBar } from '@/components/layout/SideBar';
import { SideBar } from '@/components/common/SideBar';
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
import { CitiesTable } from '@/components/metrics/CitiesTable';
import { CountriesTable } from '@/components/metrics/CountriesTable';

View file

@ -1,31 +1,80 @@
import { ReactNode } from 'react';
import { Row, Heading } from '@umami/react-zen';
import {
Column,
Row,
Heading,
MenuTrigger,
Button,
Icon,
Icons,
Popover,
Menu,
MenuItem,
MenuSeparator,
} from '@umami/react-zen';
import { Favicon } from '@/components/common/Favicon';
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
import { WebsiteTabs } from '@/app/(main)/websites/[websiteId]/WebsiteTabs';
import { useWebsite } from '@/components/hooks/useWebsite';
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { FilterTags } from '@/components/metrics/FilterTags';
import { useMessages } from '@/components/hooks';
export function WebsiteHeader({
websiteId,
children,
showFilter = true,
allowEdit = true,
compareMode = false,
}: {
websiteId: string;
children?: ReactNode;
showFilter?: boolean;
allowEdit?: boolean;
compareMode?: boolean;
}) {
const website = useWebsite();
const { formatMessage, labels } = useMessages();
const { name, domain } = website || {};
const items = [
{ label: formatMessage(labels.previousPeriod), value: 'prev' },
{ label: formatMessage(labels.previousYear), value: 'yoy' },
];
return (
<>
<Row alignItems="center" gap="3" marginY="6">
<Favicon domain={domain} />
<Heading>
{name}
<ActiveUsers websiteId={websiteId} />
</Heading>
{children}
<Column marginY="6" gap="6">
<Row alignItems="center" justifyContent="space-between" gap="3">
<Row alignItems="center" gap="3">
<Favicon domain={domain} />
<Heading>
{name}
<ActiveUsers websiteId={websiteId} />
</Heading>
</Row>
<Row alignItems="center" gap="3">
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<WebsiteDateFilter websiteId={websiteId} />
{allowEdit && (
<MenuTrigger>
<Button variant="quiet">
<Icon>
<Icons.More />
</Icon>
</Button>
<Popover placement="bottom end">
<Menu>
<MenuItem>Compare dates</MenuItem>
<MenuItem>Share</MenuItem>
<MenuSeparator />
<MenuItem>Settings</MenuItem>
</Menu>
</Popover>
</MenuTrigger>
)}
</Row>
</Row>
{compareMode && items.map(item => <div key={item.value}>{item.label}</div>)}
<FilterTags websiteId={websiteId} />
<WebsiteTabs websiteId={websiteId} />
</>
</Column>
);
}

View file

@ -1,24 +1,16 @@
import { Select, ListItem } from '@umami/react-zen';
import classNames from 'classnames';
import { useDateRange, useMessages, useSticky } from '@/components/hooks';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { useDateRange, useMessages } from '@/components/hooks';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatShortTime, formatLongNumber } from '@/lib/format';
import { useWebsiteStatsQuery } from '@/components/hooks/queries/useWebsiteStatsQuery';
import { useWebsites, setWebsiteDateCompare } from '@/store/websites';
import { WebsiteFilterButton } from './WebsiteFilterButton';
import styles from './WebsiteMetricsBar.module.css';
import { useWebsites } from '@/store/websites';
export function WebsiteMetricsBar({
websiteId,
sticky,
showChange = false,
compareMode = false,
showFilter = false,
}: {
websiteId: string;
sticky?: boolean;
showChange?: boolean;
compareMode?: boolean;
showFilter?: boolean;
@ -26,7 +18,6 @@ export function WebsiteMetricsBar({
const { dateRange } = useDateRange(websiteId);
const { formatMessage, labels } = useMessages();
const dateCompare = useWebsites(state => state[websiteId]?.dateCompare);
const { ref } = useSticky({ enabled: sticky });
const { data, isLoading, isFetched, error } = useWebsiteStatsQuery(
websiteId,
compareMode && dateCompare,
@ -76,51 +67,23 @@ export function WebsiteMetricsBar({
]
: [];
const items = [
{ label: formatMessage(labels.previousPeriod), value: 'prev' },
{ label: formatMessage(labels.previousYear), value: 'yoy' },
];
return (
<div ref={ref} className={classNames(styles.container)}>
<div>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => {
return (
<MetricCard
key={label}
value={value}
previousValue={prev}
label={label}
change={change}
formatValue={formatValue}
reverseColors={reverseColors}
showChange={!isAllTime && (compareMode || showChange)}
showPrevious={!isAllTime && compareMode}
/>
);
})}
</MetricsBar>
</div>
<div className={styles.actions}>
{showFilter && <WebsiteFilterButton websiteId={websiteId} />}
<WebsiteDateFilter websiteId={websiteId} showAllTime={!compareMode} />
{compareMode && (
<div className={styles.vs}>
<b>VS</b>
<Select
className={styles.dropdown}
items={items}
value={dateCompare || 'prev'}
onChange={(value: any) => setWebsiteDateCompare(websiteId, value)}
>
{items.map(({ label, value }) => (
<ListItem key={value}>{label}</ListItem>
))}
</Select>
</div>
)}
</div>
</div>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{metrics.map(({ label, value, prev, change, formatValue, reverseColors }) => {
return (
<MetricCard
key={label}
value={value}
previousValue={prev}
label={label}
change={change}
formatValue={formatValue}
reverseColors={reverseColors}
showChange={!isAllTime && (compareMode || showChange)}
showPrevious={!isAllTime && compareMode}
/>
);
})}
</MetricsBar>
);
}

View file

@ -1,5 +1,5 @@
import { Grid } from '@umami/react-zen';
import { Panel } from '@/components/layout/Panel';
import { Panel } from '@/components/common/Panel';
import { PagesTable } from '@/components/metrics/PagesTable';
import { ReferrersTable } from '@/components/metrics/ReferrersTable';
import { BrowsersTable } from '@/components/metrics/BrowsersTable';

View file

@ -1,28 +1,17 @@
'use client';
import { Grid } from '@umami/react-zen';
import { Panel } from '@/components/layout/Panel';
import { Panel } from '@/components/common/Panel';
import { WebsiteHeader } from '../WebsiteHeader';
import { WebsiteMetricsBar } from '../WebsiteMetricsBar';
import { FilterTags } from '@/components/metrics/FilterTags';
import { useNavigation } from '@/components/hooks';
import { FILTER_COLUMNS } from '@/lib/constants';
import { WebsiteChart } from '../WebsiteChart';
import { WebsiteCompareTables } from './WebsiteCompareTables';
export function WebsiteComparePage({ websiteId }) {
const { query } = useNavigation();
const params = Object.keys(query).reduce((obj, key) => {
if (FILTER_COLUMNS[key]) {
obj[key] = query[key];
}
return obj;
}, {});
return (
<Grid gap="3">
<WebsiteHeader websiteId={websiteId} />
<FilterTags websiteId={websiteId} params={params} />
<FilterTags websiteId={websiteId} />
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showFilter={true} />
<Panel>
<WebsiteChart websiteId={websiteId} compareMode={true} />

View file

@ -1,6 +1,6 @@
import { Grid, Heading, Column } from '@umami/react-zen';
import { useDateRange, useMessages, useNavigation } from '@/components/hooks';
import { SideBar } from '@/components/layout/SideBar';
import { SideBar } from '@/components/common/SideBar';
import { BrowsersTable } from '@/components/metrics/BrowsersTable';
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
import { CitiesTable } from '@/components/metrics/CitiesTable';

View file

@ -1,40 +1,35 @@
import { useMessages } from '@/components/hooks';
import { useWebsiteSessionStatsQuery } from '@/components/hooks/queries/useWebsiteSessionStatsQuery';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format';
import { Flexbox } from '@umami/react-zen';
export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetched, error } = useWebsiteSessionStatsQuery(websiteId);
return (
<Flexbox direction="row" justifyContent="space-between" style={{ minHeight: 120 }}>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
<MetricCard
value={data?.visitors?.value}
label={formatMessage(labels.visitors)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.visits?.value}
label={formatMessage(labels.visits)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.pageviews?.value}
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.events?.value}
label={formatMessage(labels.events)}
formatValue={formatLongNumber}
/>
</MetricsBar>
<WebsiteDateFilter websiteId={websiteId} />
</Flexbox>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
<MetricCard
value={data?.visitors?.value}
label={formatMessage(labels.visitors)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.visits?.value}
label={formatMessage(labels.visits)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.pageviews?.value}
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.events?.value}
label={formatMessage(labels.events)}
formatValue={formatLongNumber}
/>
</MetricsBar>
);
}

View file

@ -1,12 +1,12 @@
'use client';
import { TabList, Tab, Tabs, TabPanel, Grid } from '@umami/react-zen';
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
import { useState } from 'react';
import { WebsiteHeader } from '../WebsiteHeader';
import { EventsDataTable } from './EventsDataTable';
import { EventsMetricsBar } from './EventsMetricsBar';
import { Panel } from '@/components/layout/Panel';
import { Panel } from '@/components/common/Panel';
import { EventsChart } from '@/components/metrics/EventsChart';
import { GridRow } from '@/components/layout/GridRow';
import { GridRow } from '@/components/common/GridRow';
import { MetricsTable } from '@/components/metrics/MetricsTable';
import { useMessages } from '@/components/hooks';
import { EventProperties } from './EventProperties';
@ -16,9 +16,11 @@ export function EventsPage({ websiteId }) {
const { formatMessage, labels } = useMessages();
return (
<Grid gap="3">
<Column gap="3">
<WebsiteHeader websiteId={websiteId} />
<EventsMetricsBar websiteId={websiteId} />
<Panel>
<EventsMetricsBar websiteId={websiteId} />
</Panel>
<GridRow layout="two-one">
<Panel>
<EventsChart websiteId={websiteId} />
@ -46,6 +48,6 @@ export function EventsPage({ websiteId }) {
</TabPanel>
</Tabs>
</Panel>
</Grid>
</Column>
);
}

View file

@ -1,36 +1,18 @@
import { MetricCard } from '@/components/metrics/MetricCard';
import { useMessages } from '@/components/hooks';
import { RealtimeData } from '@/lib/types';
import styles from './RealtimeHeader.module.css';
import { MetricsBar } from '@/components/metrics/MetricsBar';
export function RealtimeHeader({ data }: { data: RealtimeData }) {
const { formatMessage, labels } = useMessages();
const { totals }: any = data || {};
return (
<div className={styles.header}>
<div className={styles.metrics}>
<MetricCard
className={styles.card}
label={formatMessage(labels.views)}
value={totals.views}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.visitors)}
value={totals.visitors}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.events)}
value={totals.events}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.countries)}
value={totals.countries}
/>
</div>
</div>
<MetricsBar isFetched={true}>
<MetricCard label={formatMessage(labels.views)} value={totals.views} />
<MetricCard label={formatMessage(labels.visitors)} value={totals.visitors} />
<MetricCard label={formatMessage(labels.events)} value={totals.events} />
<MetricCard label={formatMessage(labels.countries)} value={totals.countries} />
</MetricsBar>
);
}

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Page } from '@/components/layout/Page';
import { PageHeader } from '@/components/layout/PageHeader';
import { Page } from '@/components/common/Page';
import { PageHeader } from '@/components/common/PageHeader';
import { useApi, useMessages } from '@/components/hooks';
import { EmptyPlaceholder } from '@/components/common/EmptyPlaceholder';

View file

@ -1,9 +1,9 @@
'use client';
import { firstBy } from 'thenby';
import { Grid } from '@umami/react-zen';
import { GridRow } from '@/components/layout/GridRow';
import { Page } from '@/components/layout/Page';
import { Panel } from '@/components/layout/Panel';
import { GridRow } from '@/components/common/GridRow';
import { Page } from '@/components/common/Page';
import { Panel } from '@/components/common/Panel';
import { RealtimeChart } from '@/components/metrics/RealtimeChart';
import { WorldMap } from '@/components/metrics/WorldMap';
import { useRealtimeQuery } from '@/components/hooks';
@ -30,7 +30,9 @@ export function WebsiteRealtimePage({ websiteId }) {
return (
<Grid gap="3">
<WebsiteHeader websiteId={websiteId} />
<RealtimeHeader data={data} />
<Panel>
<RealtimeHeader data={data} />
</Panel>
<Panel>
<RealtimeChart data={data} unit="minute" />
</Panel>

View file

@ -1,40 +1,35 @@
import { useMessages } from '@/components/hooks';
import { useWebsiteSessionStatsQuery } from '@/components/hooks/queries/useWebsiteSessionStatsQuery';
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
import { MetricCard } from '@/components/metrics/MetricCard';
import { MetricsBar } from '@/components/metrics/MetricsBar';
import { formatLongNumber } from '@/lib/format';
import { Flexbox } from '@umami/react-zen';
export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
const { formatMessage, labels } = useMessages();
const { data, isLoading, isFetched, error } = useWebsiteSessionStatsQuery(websiteId);
return (
<Flexbox direction="row" justifyContent="space-between" style={{ minHeight: 120 }}>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
<MetricCard
value={data?.visitors?.value}
label={formatMessage(labels.visitors)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.visits?.value}
label={formatMessage(labels.visits)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.pageviews?.value}
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.countries?.value}
label={formatMessage(labels.countries)}
formatValue={formatLongNumber}
/>
</MetricsBar>
<WebsiteDateFilter websiteId={websiteId} />
</Flexbox>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
<MetricCard
value={data?.visitors?.value}
label={formatMessage(labels.visitors)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.visits?.value}
label={formatMessage(labels.visits)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.pageviews?.value}
label={formatMessage(labels.views)}
formatValue={formatLongNumber}
/>
<MetricCard
value={data?.countries?.value}
label={formatMessage(labels.countries)}
formatValue={formatLongNumber}
/>
</MetricsBar>
);
}

View file

@ -1,24 +1,26 @@
'use client';
import { useState } from 'react';
import { TabList, Tab, Tabs, TabPanel, Column } from '@umami/react-zen';
import { WebsiteHeader } from '../WebsiteHeader';
import { SessionsDataTable } from './SessionsDataTable';
import { SessionsMetricsBar } from './SessionsMetricsBar';
import { SessionProperties } from './SessionProperties';
import { WorldMap } from '@/components/metrics/WorldMap';
import { GridRow } from '@/components/layout/GridRow';
import { TabList, Tab, Tabs, TabPanel } from '@umami/react-zen';
import { useState } from 'react';
import { GridRow } from '@/components/common/GridRow';
import { useMessages } from '@/components/hooks';
import { SessionsWeekly } from './SessionsWeekly';
import { Panel } from '@/components/layout/Panel';
import { Panel } from '@/components/common/Panel';
export function SessionsPage({ websiteId }) {
const [tab, setTab] = useState('activity');
const { formatMessage, labels } = useMessages();
return (
<>
<Column gap="3">
<WebsiteHeader websiteId={websiteId} />
<SessionsMetricsBar websiteId={websiteId} />
<Panel>
<SessionsMetricsBar websiteId={websiteId} />
</Panel>
<GridRow layout="two-one">
<Panel padding="0">
<WorldMap websiteId={websiteId} />
@ -41,6 +43,6 @@ export function SessionsPage({ websiteId }) {
</TabPanel>
</Tabs>
</Panel>
</>
</Column>
);
}