mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 22:57:12 +01:00
New properties screens. New website nav.
This commit is contained in:
parent
a9a9b57f80
commit
01bfd7f52e
17 changed files with 536 additions and 557 deletions
|
|
@ -1,4 +1,3 @@
|
|||
import { ReactNode } from 'react';
|
||||
import Link from 'next/link';
|
||||
import {
|
||||
Sidebar,
|
||||
|
|
@ -18,35 +17,13 @@ import {
|
|||
Settings,
|
||||
LockKeyhole,
|
||||
PanelLeft,
|
||||
Eye,
|
||||
Lightning,
|
||||
User,
|
||||
Clock,
|
||||
Target,
|
||||
Funnel,
|
||||
Path,
|
||||
Magnet,
|
||||
Tag,
|
||||
Money,
|
||||
Network,
|
||||
Arrow,
|
||||
Sheet,
|
||||
} from '@/components/icons';
|
||||
import { useMessages, useNavigation, useGlobalState } from '@/components/hooks';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
|
||||
type NavLink = {
|
||||
id: string;
|
||||
label: string;
|
||||
path: string;
|
||||
icon: ReactNode;
|
||||
};
|
||||
|
||||
export function SideNav(props: any) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { websiteId, pathname, renderUrl } = useNavigation();
|
||||
const { pathname, renderUrl } = useNavigation();
|
||||
const [isCollapsed, setCollapsed] = useGlobalState('sidenav-collapsed');
|
||||
const isWebsite = websiteId && !pathname.includes('/settings');
|
||||
|
||||
const links = [
|
||||
{
|
||||
|
|
@ -75,81 +52,6 @@ export function SideNav(props: any) {
|
|||
},
|
||||
];
|
||||
|
||||
const websiteLinks = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: formatMessage(labels.overview),
|
||||
icon: <Eye />,
|
||||
path: '/',
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
label: formatMessage(labels.events),
|
||||
icon: <Lightning />,
|
||||
path: '/events',
|
||||
},
|
||||
{
|
||||
id: 'sessions',
|
||||
label: formatMessage(labels.sessions),
|
||||
icon: <User />,
|
||||
path: '/sessions',
|
||||
},
|
||||
{
|
||||
id: 'realtime',
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Clock />,
|
||||
path: '/realtime',
|
||||
},
|
||||
{
|
||||
id: 'breakdown',
|
||||
label: formatMessage(labels.breakdown),
|
||||
icon: <Sheet />,
|
||||
path: '/breakdown',
|
||||
},
|
||||
{
|
||||
id: 'goals',
|
||||
label: formatMessage(labels.goals),
|
||||
icon: <Target />,
|
||||
path: '/goals',
|
||||
},
|
||||
{
|
||||
id: 'funnel',
|
||||
label: formatMessage(labels.funnels),
|
||||
icon: <Funnel />,
|
||||
path: '/funnels',
|
||||
},
|
||||
{
|
||||
id: 'journeys',
|
||||
label: formatMessage(labels.journeys),
|
||||
icon: <Path />,
|
||||
path: '/journeys',
|
||||
},
|
||||
{
|
||||
id: 'retention',
|
||||
label: formatMessage(labels.retention),
|
||||
icon: <Magnet />,
|
||||
path: '/retention',
|
||||
},
|
||||
{
|
||||
id: 'utm',
|
||||
label: formatMessage(labels.utm),
|
||||
icon: <Tag />,
|
||||
path: '/utm',
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
label: formatMessage(labels.revenue),
|
||||
icon: <Money />,
|
||||
path: '/revenue',
|
||||
},
|
||||
{
|
||||
id: 'attribution',
|
||||
label: formatMessage(labels.attribution),
|
||||
icon: <Network />,
|
||||
path: '/attribution',
|
||||
},
|
||||
];
|
||||
|
||||
const bottomLinks = [
|
||||
{
|
||||
id: 'settings',
|
||||
|
|
@ -171,22 +73,22 @@ export function SideNav(props: any) {
|
|||
<SidebarHeader label="umami" icon={<Logo />} />
|
||||
</SidebarSection>
|
||||
<SidebarSection flexGrow={1}>
|
||||
{!isWebsite && <NavItems items={links} params={false} />}
|
||||
{isWebsite && (
|
||||
<>
|
||||
<Row>
|
||||
<LinkButton href={renderUrl('/websites', false)} variant="outline">
|
||||
<Icon rotate={180}>
|
||||
<Arrow />
|
||||
</Icon>
|
||||
</LinkButton>
|
||||
</Row>
|
||||
<NavItems items={websiteLinks} prefix={`/websites/${websiteId}`} />
|
||||
</>
|
||||
)}
|
||||
{links.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={renderUrl(path, false)} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.endsWith(path)} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
{!isWebsite && <NavItems items={bottomLinks} params={false} />}
|
||||
{bottomLinks.map(({ id, path, label, icon }) => {
|
||||
return (
|
||||
<Link key={id} href={path} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={pathname.startsWith(path)} />
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
<SidebarSection>
|
||||
<Row>
|
||||
|
|
@ -200,26 +102,3 @@ export function SideNav(props: any) {
|
|||
</Sidebar>
|
||||
);
|
||||
}
|
||||
|
||||
const NavItems = ({
|
||||
items,
|
||||
prefix = '',
|
||||
params,
|
||||
}: {
|
||||
items: NavLink[];
|
||||
prefix?: string;
|
||||
params?: Record<string, string | number> | false;
|
||||
}) => {
|
||||
const { renderUrl, pathname, websiteId } = useNavigation();
|
||||
|
||||
return items.map(({ id, path, label, icon }) => {
|
||||
const isSelected = websiteId
|
||||
? (path === '/' && pathname.endsWith(websiteId)) || pathname.endsWith(path)
|
||||
: pathname.startsWith(path);
|
||||
return (
|
||||
<Link key={id} href={renderUrl(`${prefix}${path}`, params)} role="button">
|
||||
<SidebarItem label={label} icon={icon} isSelected={isSelected} />
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ export function AdminLayout({ children }: { children: ReactNode }) {
|
|||
<PageBody>
|
||||
<Column gap="6">
|
||||
<PageHeader title={formatMessage(labels.admin)} />
|
||||
<Grid columns="160px 1fr" gap>
|
||||
<Grid columns="200px 1fr" gap>
|
||||
<Column>
|
||||
<SideMenu items={items} selectedKey={value} />
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export function SettingsLayout({ children }: { children: ReactNode }) {
|
|||
<PageBody>
|
||||
<Column gap="6">
|
||||
<PageHeader title={formatMessage(labels.settings)} />
|
||||
<Grid columns="160px 1fr" gap>
|
||||
<Grid columns="200px 1fr" gap>
|
||||
<Column>
|
||||
<SideMenu items={items} selectedKey={value} />
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ 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';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
|
||||
export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
||||
const {
|
||||
|
|
@ -17,7 +18,9 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
|||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<FieldsButton value={fields} onChange={setFields} />
|
||||
<SectionHeader>
|
||||
<FieldsButton value={fields} onChange={setFields} />
|
||||
</SectionHeader>
|
||||
<Panel height="900px" overflow="auto" allowFullscreen>
|
||||
<Breakdown
|
||||
websiteId={websiteId}
|
||||
|
|
@ -36,7 +39,7 @@ const FieldsButton = ({ value, onChange }) => {
|
|||
return (
|
||||
<Box>
|
||||
<DialogTrigger>
|
||||
<Button>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<ListCheck />
|
||||
</Icon>
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function UTM({ websiteId, startDate, endDate }: UTMProps) {
|
|||
});
|
||||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error} minHeight="300px">
|
||||
{data && (
|
||||
<Column gap>
|
||||
{UTM_PARAMS.map(param => {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export function WebsiteHeader() {
|
|||
const website = useWebsite();
|
||||
|
||||
return (
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />}>
|
||||
<Row alignItems="center" gap="6">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
<Row alignItems="center" gap>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,25 @@
|
|||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { Column, Grid } from '@umami/react-zen';
|
||||
import { WebsiteProvider } from './WebsiteProvider';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { WebsiteHeader } from './WebsiteHeader';
|
||||
import { WebsiteNav } from '@/app/(main)/websites/[websiteId]/WebsiteNav';
|
||||
|
||||
export function WebsiteLayout({ websiteId, children }: { websiteId: string; children: ReactNode }) {
|
||||
return (
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<PageBody>
|
||||
<WebsiteHeader />
|
||||
<Column>{children}</Column>
|
||||
</PageBody>
|
||||
<Grid columns="200px 1fr" gap width="100%">
|
||||
<Column padding>
|
||||
<WebsiteNav websiteId={websiteId} />
|
||||
</Column>
|
||||
<PageBody>
|
||||
<Column gap>
|
||||
<WebsiteHeader />
|
||||
<Column>{children}</Column>
|
||||
</Column>
|
||||
</PageBody>
|
||||
</Grid>
|
||||
</WebsiteProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Icon, Text, Row, NavMenu, NavMenuItem } from '@umami/react-zen';
|
||||
import { Icon, Text, Row, NavMenu, NavMenuItem, NavMenuGroup } from '@umami/react-zen';
|
||||
import {
|
||||
Eye,
|
||||
Lightning,
|
||||
User,
|
||||
Clock,
|
||||
Lightbulb,
|
||||
Sheet,
|
||||
Target,
|
||||
Funnel,
|
||||
Path,
|
||||
|
|
@ -22,95 +22,123 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) {
|
|||
|
||||
const links = [
|
||||
{
|
||||
id: 'overview',
|
||||
label: formatMessage(labels.overview),
|
||||
icon: <Eye />,
|
||||
path: '',
|
||||
label: formatMessage(labels.core),
|
||||
items: [
|
||||
{
|
||||
id: 'overview',
|
||||
label: formatMessage(labels.overview),
|
||||
icon: <Eye />,
|
||||
path: '',
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
label: formatMessage(labels.events),
|
||||
icon: <Lightning />,
|
||||
path: '/events',
|
||||
},
|
||||
{
|
||||
id: 'sessions',
|
||||
label: formatMessage(labels.sessions),
|
||||
icon: <User />,
|
||||
path: '/sessions',
|
||||
},
|
||||
{
|
||||
id: 'realtime',
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Clock />,
|
||||
path: '/realtime',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
label: formatMessage(labels.events),
|
||||
icon: <Lightning />,
|
||||
path: '/events',
|
||||
label: formatMessage(labels.behavior),
|
||||
items: [
|
||||
{
|
||||
id: 'goals',
|
||||
label: formatMessage(labels.goals),
|
||||
icon: <Target />,
|
||||
path: '/goals',
|
||||
},
|
||||
{
|
||||
id: 'funnel',
|
||||
label: formatMessage(labels.funnels),
|
||||
icon: <Funnel />,
|
||||
path: '/funnels',
|
||||
},
|
||||
{
|
||||
id: 'journeys',
|
||||
label: formatMessage(labels.journeys),
|
||||
icon: <Path />,
|
||||
path: '/journeys',
|
||||
},
|
||||
{
|
||||
id: 'retention',
|
||||
label: formatMessage(labels.retention),
|
||||
icon: <Magnet />,
|
||||
path: '/retention',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'sessions',
|
||||
label: formatMessage(labels.sessions),
|
||||
icon: <User />,
|
||||
path: '/sessions',
|
||||
label: formatMessage(labels.segments),
|
||||
items: [
|
||||
{
|
||||
id: 'breakdown',
|
||||
label: formatMessage(labels.breakdown),
|
||||
icon: <Sheet />,
|
||||
path: '/breakdown',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'realtime',
|
||||
label: formatMessage(labels.realtime),
|
||||
icon: <Clock />,
|
||||
path: '/realtime',
|
||||
},
|
||||
{
|
||||
id: 'insights',
|
||||
label: formatMessage(labels.insights),
|
||||
icon: <Lightbulb />,
|
||||
path: '/insights',
|
||||
},
|
||||
{
|
||||
id: 'goals',
|
||||
label: formatMessage(labels.goals),
|
||||
icon: <Target />,
|
||||
path: '/goals',
|
||||
},
|
||||
{
|
||||
id: 'funnel',
|
||||
label: formatMessage(labels.funnels),
|
||||
icon: <Funnel />,
|
||||
path: '/funnels',
|
||||
},
|
||||
{
|
||||
id: 'journeys',
|
||||
label: formatMessage(labels.journeys),
|
||||
icon: <Path />,
|
||||
path: '/journeys',
|
||||
},
|
||||
{
|
||||
id: 'retention',
|
||||
label: formatMessage(labels.retention),
|
||||
icon: <Magnet />,
|
||||
path: '/retention',
|
||||
},
|
||||
{
|
||||
id: 'utm',
|
||||
label: formatMessage(labels.utm),
|
||||
icon: <Tag />,
|
||||
path: '/utm',
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
label: formatMessage(labels.revenue),
|
||||
icon: <Money />,
|
||||
path: '/revenue',
|
||||
},
|
||||
{
|
||||
id: 'attribution',
|
||||
label: formatMessage(labels.attribution),
|
||||
icon: <Network />,
|
||||
path: '/attribution',
|
||||
label: formatMessage(labels.growth),
|
||||
items: [
|
||||
{
|
||||
id: 'utm',
|
||||
label: formatMessage(labels.utm),
|
||||
icon: <Tag />,
|
||||
path: '/utm',
|
||||
},
|
||||
{
|
||||
id: 'revenue',
|
||||
label: formatMessage(labels.revenue),
|
||||
icon: <Money />,
|
||||
path: '/revenue',
|
||||
},
|
||||
{
|
||||
id: 'attribution',
|
||||
label: formatMessage(labels.attribution),
|
||||
icon: <Network />,
|
||||
path: '/attribution',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const selected = links.find(({ path }) => path && pathname.endsWith(path))?.id || 'overview';
|
||||
const selected =
|
||||
links.flatMap(e => e.items).find(({ path }) => path && pathname.endsWith(path))?.id ||
|
||||
'overview';
|
||||
|
||||
return (
|
||||
<NavMenu highlightColor="3">
|
||||
{links.map(({ id, label, icon, path }) => {
|
||||
const isSelected = selected === id;
|
||||
|
||||
<NavMenu highlightColor="3" style={{ position: 'sticky', top: 'var(--spacing-2)' }}>
|
||||
{links.map(({ label, items }) => {
|
||||
return (
|
||||
<Link key={id} href={renderUrl(`/websites/${websiteId}${path}`)}>
|
||||
<NavMenuItem isSelected={isSelected}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
</Row>
|
||||
</NavMenuItem>
|
||||
</Link>
|
||||
<NavMenuGroup title={label} key={label} gap="2">
|
||||
{items.map(({ id, label, icon, path }) => {
|
||||
const isSelected = selected === id;
|
||||
|
||||
return (
|
||||
<Link key={id} href={renderUrl(`/websites/${websiteId}${path}`)}>
|
||||
<NavMenuItem isSelected={isSelected}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>{icon}</Icon>
|
||||
<Text>{label}</Text>
|
||||
</Row>
|
||||
</NavMenuItem>
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</NavMenuGroup>
|
||||
);
|
||||
})}
|
||||
</NavMenu>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,5 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import {
|
||||
DataColumn,
|
||||
DataTable,
|
||||
Row,
|
||||
Loading,
|
||||
Column,
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
Text,
|
||||
} from '@umami/react-zen';
|
||||
import { Select, ListItem, Grid } from '@umami/react-zen';
|
||||
import {
|
||||
useEventDataPropertiesQuery,
|
||||
useEventDataValuesQuery,
|
||||
|
|
@ -22,11 +13,69 @@ import { ListTable } from '@/components/metrics/ListTable';
|
|||
export function EventProperties({ websiteId }: { websiteId: string }) {
|
||||
const [propertyName, setPropertyName] = useState('');
|
||||
const [eventName, setEventName] = useState('');
|
||||
const [propertyView, setPropertyView] = useState('table');
|
||||
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, isLoading, isFetching, error } = useEventDataPropertiesQuery(websiteId);
|
||||
const { data: values } = useEventDataValuesQuery(websiteId, eventName, propertyName);
|
||||
|
||||
const events: string[] = data
|
||||
? data.reduce((arr: string | any[], e: { eventName: any }) => {
|
||||
return !arr.includes(e.eventName) ? arr.concat(e.eventName) : arr;
|
||||
}, [])
|
||||
: [];
|
||||
const properties: string[] = eventName
|
||||
? data?.filter(e => e.eventName === eventName).map(e => e.propertyName)
|
||||
: [];
|
||||
|
||||
return (
|
||||
<LoadingPanel
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
data={data}
|
||||
error={error}
|
||||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="repeat(auto-fill, minmax(300px, 1fr))" gap>
|
||||
<Select
|
||||
label={formatMessage(labels.event)}
|
||||
value={eventName}
|
||||
onChange={setEventName}
|
||||
placeholder=""
|
||||
>
|
||||
{events?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
<Select
|
||||
label={formatMessage(labels.property)}
|
||||
value={propertyName}
|
||||
onChange={setPropertyName}
|
||||
isDisabled={!eventName}
|
||||
placeholder=""
|
||||
>
|
||||
{properties?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
{propertyName && (
|
||||
<EventValues websiteId={websiteId} eventName={eventName} propertyName={propertyName} />
|
||||
)}
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const EventValues = ({ websiteId, eventName, propertyName }) => {
|
||||
const {
|
||||
data: values,
|
||||
isLoading,
|
||||
isFetching,
|
||||
error,
|
||||
} = useEventDataValuesQuery(websiteId, eventName, propertyName);
|
||||
|
||||
const propertySum = useMemo(() => {
|
||||
return values?.reduce((sum, { total }) => sum + total, 0) ?? 0;
|
||||
|
|
@ -55,43 +104,19 @@ export function EventProperties({ websiteId }: { websiteId: string }) {
|
|||
}));
|
||||
}, [propertyName, values, propertySum]);
|
||||
|
||||
const handleRowClick = row => {
|
||||
setEventName(row.eventName);
|
||||
setPropertyName(row.propertyName);
|
||||
};
|
||||
|
||||
return (
|
||||
<LoadingPanel isLoading={isLoading} isFetching={isFetching} data={data} error={error}>
|
||||
<Column>
|
||||
<DataTable data={data}>
|
||||
<DataColumn id="eventName" label={formatMessage(labels.name)}>
|
||||
{(row: any) => <Row onClick={() => handleRowClick(row)}>{row.eventName}</Row>}
|
||||
</DataColumn>
|
||||
<DataColumn id="propertyName" label={formatMessage(labels.property)}>
|
||||
{(row: any) => <Row onClick={() => handleRowClick(row)}>{row.propertyName}</Row>}
|
||||
</DataColumn>
|
||||
<DataColumn id="total" label={formatMessage(labels.count)} align="end" />
|
||||
</DataTable>
|
||||
{propertyName && (
|
||||
<Column>
|
||||
<Row gap justifyContent="space-between">
|
||||
<Text>{`${eventName}: ${propertyName}`}</Text>
|
||||
<ToggleGroup value={[propertyView]} onChange={value => setPropertyView(value[0])}>
|
||||
<ToggleGroupItem id="table">{formatMessage(labels.table)}</ToggleGroupItem>
|
||||
<ToggleGroupItem id="chart">{formatMessage(labels.chart)}</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</Row>
|
||||
|
||||
{!values ? (
|
||||
<Loading icon="dots" />
|
||||
) : propertyView === 'table' ? (
|
||||
<ListTable data={tableData} />
|
||||
) : (
|
||||
<PieChart key={propertyName + eventName} type="doughnut" chartData={chartData} />
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
</Column>
|
||||
<LoadingPanel
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
data={values}
|
||||
error={error}
|
||||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
{values && <ListTable title={propertyName} data={tableData} />}
|
||||
<PieChart key={propertyName + eventName} type="doughnut" chartData={chartData} />
|
||||
</Grid>
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { useState } from 'react';
|
|||
import { EventsDataTable } from './EventsDataTable';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { EventsChart } from '@/components/metrics/EventsChart';
|
||||
import { GridRow } from '@/components/common/GridRow';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { EventProperties } from './EventProperties';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
|
|
@ -22,29 +21,28 @@ export function EventsPage({ websiteId }) {
|
|||
return (
|
||||
<Column gap="3">
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<GridRow layout="two-one">
|
||||
<Panel gridColumn="span 2">
|
||||
<EventsChart websiteId={websiteId} focusLabel={label} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<EventsTable
|
||||
websiteId={websiteId}
|
||||
type="event"
|
||||
title={formatMessage(labels.events)}
|
||||
metric={formatMessage(labels.actions)}
|
||||
onLabelClick={handleLabelClick}
|
||||
/>
|
||||
</Panel>
|
||||
</GridRow>
|
||||
<Panel>
|
||||
<Tabs selectedKey={tab} onSelectionChange={(value: any) => setTab(value)}>
|
||||
<TabList>
|
||||
<Tab id="activity">{formatMessage(labels.activity)}</Tab>
|
||||
<Tab id="chart">{formatMessage(labels.chart)}</Tab>
|
||||
<Tab id="properties">{formatMessage(labels.properties)}</Tab>
|
||||
</TabList>
|
||||
<TabPanel id="activity">
|
||||
<EventsDataTable websiteId={websiteId} />
|
||||
</TabPanel>
|
||||
<TabPanel id="chart">
|
||||
<Column gap="6">
|
||||
<EventsChart websiteId={websiteId} focusLabel={label} />
|
||||
<EventsTable
|
||||
websiteId={websiteId}
|
||||
type="event"
|
||||
title={formatMessage(labels.events)}
|
||||
metric={formatMessage(labels.actions)}
|
||||
onLabelClick={handleLabelClick}
|
||||
/>
|
||||
</Column>
|
||||
</TabPanel>
|
||||
<TabPanel id="properties">
|
||||
<EventProperties websiteId={websiteId} />
|
||||
</TabPanel>
|
||||
|
|
|
|||
|
|
@ -1,51 +1,93 @@
|
|||
import { Grid, DataColumn, DataTable } from '@umami/react-zen';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Select, ListItem, Grid } from '@umami/react-zen';
|
||||
import {
|
||||
useMessages,
|
||||
useSessionDataPropertiesQuery,
|
||||
useSessionDataValuesQuery,
|
||||
useMessages,
|
||||
} from '@/components/hooks';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { PieChart } from '@/components/charts/PieChart';
|
||||
import { useState } from 'react';
|
||||
import { CHART_COLORS } from '@/lib/constants';
|
||||
import { ListTable } from '@/components/metrics/ListTable';
|
||||
|
||||
export function SessionProperties({ websiteId }: { websiteId: string }) {
|
||||
const [propertyName, setPropertyName] = useState('');
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { data, isLoading, isFetching, error } = useSessionDataPropertiesQuery(websiteId);
|
||||
const { data: values } = useSessionDataValuesQuery(websiteId, propertyName);
|
||||
const chartData =
|
||||
propertyName && values
|
||||
? {
|
||||
labels: values.map(({ value }) => value),
|
||||
datasets: [
|
||||
{
|
||||
data: values.map(({ total }) => total),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
}
|
||||
: null;
|
||||
|
||||
const properties: string[] = data?.map(e => e.propertyName);
|
||||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
|
||||
<Grid>
|
||||
<DataTable data={data}>
|
||||
<DataColumn id="propertyName" label={formatMessage(labels.property)}>
|
||||
{(row: any) => (
|
||||
<div onClick={() => setPropertyName(row.propertyName)}>{row.propertyName}</div>
|
||||
)}
|
||||
</DataColumn>
|
||||
<DataColumn id="total" label={formatMessage(labels.count)} align="end" />
|
||||
</DataTable>
|
||||
{propertyName && (
|
||||
<div>
|
||||
<div>{propertyName}</div>
|
||||
<PieChart key={propertyName} type="doughnut" chartData={chartData} />
|
||||
</div>
|
||||
)}
|
||||
<LoadingPanel
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
data={data}
|
||||
error={error}
|
||||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="repeat(auto-fill, minmax(300px, 1fr))" gap>
|
||||
<Select
|
||||
label={formatMessage(labels.event)}
|
||||
value={propertyName}
|
||||
onChange={setPropertyName}
|
||||
placeholder=""
|
||||
>
|
||||
{properties?.map(p => (
|
||||
<ListItem key={p} id={p}>
|
||||
{p}
|
||||
</ListItem>
|
||||
))}
|
||||
</Select>
|
||||
</Grid>
|
||||
{propertyName && <SessionValues websiteId={websiteId} propertyName={propertyName} />}
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const SessionValues = ({ websiteId, propertyName }) => {
|
||||
const { data, isLoading, isFetching, error } = useSessionDataValuesQuery(websiteId, propertyName);
|
||||
|
||||
const propertySum = useMemo(() => {
|
||||
return data?.reduce((sum, { total }) => sum + total, 0) ?? 0;
|
||||
}, [data]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (!propertyName || !data) return null;
|
||||
return {
|
||||
labels: data.map(({ value }) => value),
|
||||
datasets: [
|
||||
{
|
||||
data: data.map(({ total }) => total),
|
||||
backgroundColor: CHART_COLORS,
|
||||
borderWidth: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [propertyName, data]);
|
||||
|
||||
const tableData = useMemo(() => {
|
||||
if (!propertyName || !data || propertySum === 0) return [];
|
||||
return data.map(({ value, total }) => ({
|
||||
x: value,
|
||||
y: total,
|
||||
z: 100 * (total / propertySum),
|
||||
}));
|
||||
}, [propertyName, data, propertySum]);
|
||||
|
||||
return (
|
||||
<LoadingPanel
|
||||
isLoading={isLoading}
|
||||
isFetching={isFetching}
|
||||
data={data}
|
||||
error={error}
|
||||
minHeight="300px"
|
||||
gap="6"
|
||||
>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
{data && <ListTable title={propertyName} data={tableData} />}
|
||||
<PieChart key={propertyName} type="doughnut" chartData={chartData} />
|
||||
</Grid>
|
||||
</LoadingPanel>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
import { ReactNode } from 'react';
|
||||
import { AlertBanner, Loading, Column } from '@umami/react-zen';
|
||||
import { AlertBanner, Loading, Column, ColumnProps } from '@umami/react-zen';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
|
||||
export function PageBody({
|
||||
|
|
@ -14,7 +14,7 @@ export function PageBody({
|
|||
error?: unknown;
|
||||
isLoading?: boolean;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
} & ColumnProps) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
|
||||
if (error) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ export function useFields() {
|
|||
{ name: 'path', type: 'string', label: formatMessage(labels.path) },
|
||||
// { name: 'cohort', type: 'string', label: formatMessage(labels.cohort) },
|
||||
// { name: 'segment', type: 'string', label: formatMessage(labels.segment) },
|
||||
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
|
||||
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
|
||||
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
|
||||
//{ name: 'query', type: 'string', label: formatMessage(labels.query) },
|
||||
|
|
|
|||
|
|
@ -338,6 +338,9 @@ export const labels = defineMessages({
|
|||
location: { id: 'label.location', defaultMessage: 'Location' },
|
||||
chart: { id: 'label.chart', defaultMessage: 'Chart' },
|
||||
table: { id: 'label.table', defaultMessage: 'Table' },
|
||||
core: { id: 'label.core', defaultMessage: 'Core' },
|
||||
behavior: { id: 'label.behavior', defaultMessage: 'Behavior' },
|
||||
growth: { id: 'label.growth', defaultMessage: 'Growth' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ async function relationalQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
) {
|
||||
const { rawQuery, parseFilters, getDateSQL } = prisma;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters(filters);
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
@ -49,7 +49,7 @@ async function clickhouseQuery(
|
|||
filters: QueryFilters & { propertyName?: string },
|
||||
): Promise<{ propertyName: string; dataType: number; propertyValue: string; total: number }[]> {
|
||||
const { rawQuery, parseFilters } = clickhouse;
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters(filters);
|
||||
const { filterQuery, cohortQuery, queryParams } = parseFilters({ ...filters, websiteId });
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue