mirror of
https://github.com/umami-software/umami.git
synced 2026-02-06 21:57:16 +01:00
Update Retention report.
This commit is contained in:
parent
184a387ecd
commit
ee8750d9df
22 changed files with 214 additions and 280 deletions
|
|
@ -1,17 +0,0 @@
|
|||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.chart {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--font-size-lg);
|
||||
line-height: 60px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
.container {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
width: 200px;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--base800);
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
|
@ -2,23 +2,27 @@ import { Column, Row } from '@umami/react-zen';
|
|||
import { WebsiteFilterButton } from '@/app/(main)/websites/[websiteId]/WebsiteFilterButton';
|
||||
import { WebsiteDateFilter } from '@/components/input/WebsiteDateFilter';
|
||||
import { FilterBar } from '@/components/input/FilterBar';
|
||||
import { WebsiteMonthSelect } from '@/components/input/WebsiteMonthSelect';
|
||||
|
||||
export function WebsiteControls({
|
||||
websiteId,
|
||||
allowFilter = true,
|
||||
allowDateFilter = true,
|
||||
allowMonthFilter,
|
||||
allowCompare,
|
||||
}: {
|
||||
websiteId: string;
|
||||
allowFilter?: boolean;
|
||||
allowCompare?: boolean;
|
||||
allowDateFilter?: boolean;
|
||||
allowMonthFilter?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Column gap>
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||
{allowFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||
<Row alignItems="center" gap="3">
|
||||
<WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />
|
||||
</Row>
|
||||
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
||||
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
||||
</Row>
|
||||
<FilterBar />
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -14,15 +14,17 @@ export function WebsiteHeader() {
|
|||
|
||||
return (
|
||||
<PageHeader title={website.name} icon={<Favicon domain={website.domain} />} showBorder={false}>
|
||||
<Row alignItems="center" gap>
|
||||
<Row alignItems="center" gap="6">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||
<LinkButton href={renderUrl(`/settings/websites/${website.id}`)}>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>Edit</Text>
|
||||
</LinkButton>
|
||||
<Row alignItems="center" gap>
|
||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||
<LinkButton href={renderUrl(`/settings/websites/${website.id}`)}>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>Edit</Text>
|
||||
</LinkButton>
|
||||
</Row>
|
||||
</Row>
|
||||
</PageHeader>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,52 +0,0 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: var(--base50);
|
||||
z-index: var(--z-index-above);
|
||||
min-height: 120px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.vs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-basis: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1200px) {
|
||||
.container {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 992px) {
|
||||
.sticky {
|
||||
position: sticky;
|
||||
top: -1px;
|
||||
}
|
||||
|
||||
.isSticky {
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--base300);
|
||||
}
|
||||
}
|
||||
|
|
@ -11,28 +11,30 @@ export function EventsMetricsBar({ websiteId }: { websiteId: string }) {
|
|||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
|
||||
<MetricsBar>
|
||||
<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>
|
||||
{data && (
|
||||
<MetricsBar>
|
||||
<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>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
country: string;
|
||||
device: string;
|
||||
}) => {
|
||||
const { __type, eventName, urlPath: url, browser, os, country, device } = log;
|
||||
const { __type, eventName, urlPath, browser, os, country, device } = log;
|
||||
|
||||
if (__type === TYPE_EVENT) {
|
||||
return formatMessage(messages.eventLog, {
|
||||
|
|
@ -75,12 +75,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
url: (
|
||||
<a
|
||||
key="a"
|
||||
href={`//${website?.domain}${url}`}
|
||||
href={`//${website?.domain}${urlPath}`}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{url}
|
||||
{urlPath}
|
||||
</a>
|
||||
),
|
||||
});
|
||||
|
|
@ -89,12 +89,12 @@ export function RealtimeLog({ data }: { data: RealtimeData }) {
|
|||
if (__type === TYPE_PAGEVIEW) {
|
||||
return (
|
||||
<a
|
||||
href={`//${website?.domain}${url}`}
|
||||
href={`//${website?.domain}${urlPath}`}
|
||||
className={styles.link}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{url}
|
||||
{urlPath}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,13 @@ export function ReportsNav({ websiteId }: { websiteId: string }) {
|
|||
const isSelected = selected === id;
|
||||
|
||||
return (
|
||||
<Link key={id} href={renderUrl(`/websites/${websiteId}/reports${path}`)}>
|
||||
<Link
|
||||
key={id}
|
||||
href={renderUrl(
|
||||
`/websites/${websiteId}/reports${path}`,
|
||||
path === '/retention' ? false : null,
|
||||
)}
|
||||
>
|
||||
<NavMenuItem isSelected={isSelected}>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>{icon}</Icon>
|
||||
|
|
|
|||
|
|
@ -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 } from '@/components/hooks';
|
||||
import { useMessages, useLocale, useResultQuery, useTimezone } from '@/components/hooks';
|
||||
import { formatDate } from '@/lib/date';
|
||||
import { formatLongNumber } from '@/lib/format';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
|
|
@ -19,8 +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', {
|
||||
websiteId,
|
||||
timezone,
|
||||
dateRange: {
|
||||
startDate,
|
||||
endDate,
|
||||
|
|
@ -51,54 +53,56 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
|
|||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
<Panel allowFullscreen height="900px">
|
||||
<Column gap="1" width="100%" overflow="auto">
|
||||
<Grid
|
||||
columns="120px repeat(10, 100px)"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
height="50px"
|
||||
autoFlow="column"
|
||||
>
|
||||
<Column>
|
||||
<Text weight="bold" align="center">
|
||||
{formatMessage(labels.cohort)}
|
||||
</Text>
|
||||
</Column>
|
||||
{days.map(n => (
|
||||
<Column key={n}>
|
||||
<Text weight="bold" align="center" wrap="nowrap">
|
||||
{formatMessage(labels.day)} {n}
|
||||
{data && (
|
||||
<Panel allowFullscreen height="900px">
|
||||
<Column gap="1" width="100%" overflow="auto">
|
||||
<Grid
|
||||
columns="120px repeat(10, 100px)"
|
||||
alignItems="center"
|
||||
gap="1"
|
||||
height="50px"
|
||||
autoFlow="column"
|
||||
>
|
||||
<Column>
|
||||
<Text weight="bold" align="center">
|
||||
{formatMessage(labels.cohort)}
|
||||
</Text>
|
||||
</Column>
|
||||
))}
|
||||
</Grid>
|
||||
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
|
||||
return (
|
||||
<Grid key={rowIndex} columns="120px repeat(10, 100px)" gap="1" autoFlow="column">
|
||||
<Column justifyContent="center" gap="1">
|
||||
<Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Users />
|
||||
</Icon>
|
||||
<Text>{formatLongNumber(visitors)}</Text>
|
||||
</Row>
|
||||
{days.map(n => (
|
||||
<Column key={n}>
|
||||
<Text weight="bold" align="center" wrap="nowrap">
|
||||
{formatMessage(labels.day)} {n}
|
||||
</Text>
|
||||
</Column>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
return null;
|
||||
}
|
||||
const percentage = records.filter(a => a.day === day)[0]?.percentage;
|
||||
return (
|
||||
<Cell key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</Cell>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Column>
|
||||
</Panel>
|
||||
))}
|
||||
</Grid>
|
||||
{rows.map(({ date, visitors, records }: any, rowIndex: number) => {
|
||||
return (
|
||||
<Grid key={rowIndex} columns="120px repeat(10, 100px)" gap="1" autoFlow="column">
|
||||
<Column justifyContent="center" gap="1">
|
||||
<Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Users />
|
||||
</Icon>
|
||||
<Text>{formatLongNumber(visitors)}</Text>
|
||||
</Row>
|
||||
</Column>
|
||||
{days.map(day => {
|
||||
if (totalDays - rowIndex < day) {
|
||||
return null;
|
||||
}
|
||||
const percentage = records.filter(a => a.day === day)[0]?.percentage;
|
||||
return (
|
||||
<Cell key={day}>{percentage ? `${Number(percentage).toFixed(2)}%` : ''}</Cell>
|
||||
);
|
||||
})}
|
||||
</Grid>
|
||||
);
|
||||
})}
|
||||
</Column>
|
||||
</Panel>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@ import { Column } from '@umami/react-zen';
|
|||
import { Retention } from './Retention';
|
||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { useDateRange } from '@/components/hooks';
|
||||
import { endOfMonth, startOfMonth } from 'date-fns';
|
||||
|
||||
export function RetentionPage({ websiteId }: { websiteId: string }) {
|
||||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
dateRange: { startDate },
|
||||
} = useDateRange(websiteId);
|
||||
|
||||
const monthStartDate = startOfMonth(startDate);
|
||||
const monthEndDate = endOfMonth(startDate);
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Retention websiteId={websiteId} startDate={startDate} endDate={endDate} />
|
||||
<WebsiteControls websiteId={websiteId} allowDateFilter={false} allowMonthFilter />
|
||||
<Retention websiteId={websiteId} startDate={monthStartDate} endDate={monthEndDate} />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,28 +11,30 @@ export function SessionsMetricsBar({ websiteId }: { websiteId: string }) {
|
|||
|
||||
return (
|
||||
<LoadingPanel data={data} isLoading={isLoading} isFetching={isFetching} error={error}>
|
||||
<MetricsBar>
|
||||
<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>
|
||||
{data && (
|
||||
<MetricsBar>
|
||||
<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>
|
||||
)}
|
||||
</LoadingPanel>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ export async function POST(request: Request) {
|
|||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
timezone,
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
|
|
@ -23,6 +24,7 @@ export async function POST(request: Request) {
|
|||
const data = await getRetention(websiteId, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
timezone,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue