mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Fixed retention report showing wrong dates. Changed Breakdown field select to modal.
This commit is contained in:
parent
ee8750d9df
commit
ea83afbc13
20 changed files with 108 additions and 277 deletions
|
|
@ -24,7 +24,7 @@ import { Panel } from '@/components/common/Panel';
|
|||
import { DateDisplay } from '@/components/common/DateDisplay';
|
||||
|
||||
const views = {
|
||||
url: PagesTable,
|
||||
path: PagesTable,
|
||||
title: PagesTable,
|
||||
referrer: ReferrersTable,
|
||||
browser: BrowsersTable,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ export function WebsiteControls({
|
|||
return (
|
||||
<Column gap>
|
||||
<Row alignItems="center" justifyContent="space-between" gap="3">
|
||||
{allowFilter && <WebsiteFilterButton websiteId={websiteId} />}
|
||||
{allowFilter ? <WebsiteFilterButton websiteId={websiteId} /> : <div />}
|
||||
{allowDateFilter && <WebsiteDateFilter websiteId={websiteId} allowCompare={allowCompare} />}
|
||||
{allowMonthFilter && <WebsiteMonthSelect websiteId={websiteId} />}
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -30,9 +30,9 @@ export function WebsiteMetricsBar({
|
|||
const metrics = data
|
||||
? [
|
||||
{
|
||||
value: pageviews,
|
||||
label: formatMessage(labels.views),
|
||||
change: pageviews - previous.pageviews,
|
||||
value: visitors,
|
||||
label: formatMessage(labels.visitors),
|
||||
change: visitors - previous.visitors,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
|
|
@ -42,9 +42,9 @@ export function WebsiteMetricsBar({
|
|||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
value: visitors,
|
||||
label: formatMessage(labels.visitors),
|
||||
change: visitors - previous.visitors,
|
||||
value: pageviews,
|
||||
label: formatMessage(labels.views),
|
||||
change: pageviews - previous.pageviews,
|
||||
formatValue: formatLongNumber,
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -46,14 +46,14 @@ export function Breakdown({ websiteId, parameters, startDate, endDate }: Breakdo
|
|||
</DataColumn>
|
||||
);
|
||||
})}
|
||||
<DataColumn id="views" label={formatMessage(labels.views)} align="end">
|
||||
{row => row?.['views']?.toLocaleString()}
|
||||
<DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
|
||||
{row => row?.['visitors']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="visits" label={formatMessage(labels.visits)} align="end">
|
||||
{row => row?.['visits']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="visitors" label={formatMessage(labels.visitors)} align="end">
|
||||
{row => row?.['visitors']?.toLocaleString()}
|
||||
<DataColumn id="views" label={formatMessage(labels.views)} align="end">
|
||||
{row => row?.['views']?.toLocaleString()}
|
||||
</DataColumn>
|
||||
<DataColumn id="bounceRate" label={formatMessage(labels.bounceRate)} align="end">
|
||||
{row => {
|
||||
|
|
|
|||
|
|
@ -1,22 +1,12 @@
|
|||
'use client';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
Button,
|
||||
Column,
|
||||
Box,
|
||||
Grid,
|
||||
Text,
|
||||
Icon,
|
||||
Popover,
|
||||
DialogTrigger,
|
||||
} from '@umami/react-zen';
|
||||
import { useDateRange, useMessages, useFields } from '@/components/hooks';
|
||||
import { SquarePlus, Chevron } from '@/components/icons';
|
||||
import { Button, Column, Box, Text, Icon, DialogTrigger, Modal, Dialog } from '@umami/react-zen';
|
||||
import { useDateRange, useMessages } from '@/components/hooks';
|
||||
import { ListCheck } from '@/components/icons';
|
||||
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';
|
||||
|
||||
export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
||||
const {
|
||||
|
|
@ -27,9 +17,7 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
|||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<Box>
|
||||
<FieldsButton value={fields} onChange={setFields} />
|
||||
</Box>
|
||||
<FieldsButton value={fields} onChange={setFields} />
|
||||
<Panel height="900px" overflow="auto" allowFullscreen>
|
||||
<Breakdown
|
||||
websiteId={websiteId}
|
||||
|
|
@ -43,55 +31,25 @@ export function BreakdownPage({ websiteId }: { websiteId: string }) {
|
|||
}
|
||||
|
||||
const FieldsButton = ({ value, onChange }) => {
|
||||
const [selected, setSelected] = useState(value);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { fields } = useFields();
|
||||
|
||||
const handleChange = value => {
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
setIsOpen(false);
|
||||
onChange?.(selected);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogTrigger>
|
||||
<Button variant="quiet" onPress={() => setIsOpen(!isOpen)}>
|
||||
<Icon>
|
||||
<SquarePlus />
|
||||
</Icon>
|
||||
<Text>Fields</Text>
|
||||
<Icon rotate={90}>
|
||||
<Chevron />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popover placement="bottom start" isOpen={isOpen}>
|
||||
<Column width="300px" padding="2" border borderRadius shadow="3" backgroundColor gap>
|
||||
<List value={selected} onChange={handleChange} selectionMode="multiple">
|
||||
{fields.map(({ name, label }) => {
|
||||
return (
|
||||
<ListItem key={name} id={name}>
|
||||
{label}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Button onPress={handleClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<Button onPress={handleApply} variant="primary">
|
||||
{formatMessage(labels.apply)}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Column>
|
||||
</Popover>
|
||||
</DialogTrigger>
|
||||
<Box>
|
||||
<DialogTrigger>
|
||||
<Button>
|
||||
<Icon>
|
||||
<ListCheck />
|
||||
</Icon>
|
||||
<Text>Fields</Text>
|
||||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.fields)}>
|
||||
{({ close }) => (
|
||||
<FieldSelectForm selectedFields={value} onChange={onChange} onClose={close} />
|
||||
)}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</DialogTrigger>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { Column, List, ListItem, Grid, Button } from '@umami/react-zen';
|
||||
import { useFields, useMessages } from '@/components/hooks';
|
||||
import { useState } from 'react';
|
||||
|
||||
export function FieldSelectForm({
|
||||
selectedFields = [],
|
||||
onChange,
|
||||
onClose,
|
||||
}: {
|
||||
selectedFields?: string[];
|
||||
onChange: (values: string[]) => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const [selected, setSelected] = useState(selectedFields);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { fields } = useFields();
|
||||
|
||||
const handleChange = (value: string[]) => {
|
||||
setSelected(value);
|
||||
};
|
||||
|
||||
const handleApply = () => {
|
||||
onChange?.(selected);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Column width="300px" gap="6">
|
||||
<List value={selected} onChange={handleChange} selectionMode="multiple">
|
||||
{fields.map(({ name, label }) => {
|
||||
return (
|
||||
<ListItem key={name} id={name}>
|
||||
{label}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
<Grid columns="1fr 1fr" gap>
|
||||
<Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
<Button onPress={handleApply} variant="primary">
|
||||
{formatMessage(labels.apply)}
|
||||
</Button>
|
||||
</Grid>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
@ -69,7 +69,7 @@ export function GoalEditForm({
|
|||
label={formatMessage(labels.name)}
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField />
|
||||
<TextField autoFocus />
|
||||
</FormField>
|
||||
<FormField
|
||||
name="type"
|
||||
|
|
|
|||
|
|
@ -22,10 +22,10 @@ export function Retention({ websiteId, days = DAYS, startDate, endDate }: Retent
|
|||
const { timezone } = useTimezone();
|
||||
const { data, error, isLoading } = useResultQuery<any>('retention', {
|
||||
websiteId,
|
||||
timezone,
|
||||
dateRange: {
|
||||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export async function POST(request: Request) {
|
|||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
parameters: { fields },
|
||||
filters,
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
|
|
@ -24,6 +25,7 @@ export async function POST(request: Request) {
|
|||
const data = await getBreakdown(websiteId, fields, {
|
||||
startDate: new Date(startDate),
|
||||
endDate: new Date(endDate),
|
||||
...filters,
|
||||
});
|
||||
|
||||
return json(data);
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ export async function POST(request: Request) {
|
|||
|
||||
const {
|
||||
websiteId,
|
||||
dateRange: { startDate, endDate },
|
||||
timezone,
|
||||
dateRange: { startDate, endDate, timezone },
|
||||
} = body;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
.bar {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
color: var(--base600);
|
||||
}
|
||||
|
||||
.link span {
|
||||
color: var(--base700) !important;
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { Fragment } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Row, Icon, Text } from '@umami/react-zen';
|
||||
import { Chevron } from '@/components/icons';
|
||||
import styles from './Breadcrumb.module.css';
|
||||
|
||||
export interface BreadcrumbProps {
|
||||
data: {
|
||||
url?: string;
|
||||
label: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function Breadcrumb({ data }: BreadcrumbProps) {
|
||||
return (
|
||||
<Row alignItems="center" gap className={styles.bar}>
|
||||
{data.map((a, i) => {
|
||||
return (
|
||||
<Fragment key={i}>
|
||||
{a.url ? (
|
||||
<Link href={a.url} className={styles.link}>
|
||||
<Text>{a.label}</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<Text>{a.label}</Text>
|
||||
)}
|
||||
{i !== data.length - 1 ? (
|
||||
<Icon rotate={270}>
|
||||
<Chevron />
|
||||
</Icon>
|
||||
) : null}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
.search {
|
||||
max-width: 300px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.body td {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
min-height: 70px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.body > div > div > div {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.pager {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
.button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
white-space: nowrap;
|
||||
gap: var(--size200);
|
||||
font-family: inherit;
|
||||
color: var(--base900);
|
||||
background: var(--base100);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--border-radius);
|
||||
min-height: var(--base-height);
|
||||
padding: 0 var(--size600);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: var(--base200);
|
||||
}
|
||||
|
||||
.button:active {
|
||||
background: var(--base300);
|
||||
}
|
||||
|
||||
.button:visited {
|
||||
color: var(--base900);
|
||||
}
|
||||
|
||||
.button.disabled {
|
||||
color: var(--disabled-color) !important;
|
||||
background-color: var(--disabled-background) !important;
|
||||
border-color: transparent !important;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.button.primary {
|
||||
color: var(--light50);
|
||||
background: var(--primary-color);
|
||||
}
|
||||
|
||||
.button.primary:hover {
|
||||
color: var(--light50);
|
||||
background: var(--primary500);
|
||||
}
|
||||
|
||||
.button.primary:active {
|
||||
color: var(--light50);
|
||||
background: var(--primary600);
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--base50);
|
||||
}
|
||||
|
||||
.button.secondary:hover {
|
||||
background: var(--base75);
|
||||
}
|
||||
|
||||
.button.secondary:active {
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.button.quiet {
|
||||
color: var(--base900);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.button.quiet:hover {
|
||||
background: var(--base100);
|
||||
}
|
||||
|
||||
.button.quiet:active {
|
||||
background: var(--base200);
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
color: var(--light50);
|
||||
background: var(--red800);
|
||||
}
|
||||
|
||||
.button.danger:hover {
|
||||
color: var(--light50);
|
||||
background: var(--red900);
|
||||
}
|
||||
|
||||
.button.danger:active {
|
||||
color: var(--light50);
|
||||
background: var(--red1000);
|
||||
}
|
||||
|
||||
.button.size-sm {
|
||||
font-size: var(--font-size-sm);
|
||||
height: calc(var(--base-height) * 0.75);
|
||||
padding: 0 calc(var(--size600) * 0.75);
|
||||
}
|
||||
|
||||
.button.size-md {
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
|
||||
.button.size-lg {
|
||||
font-size: var(--font-size-lg);
|
||||
height: calc(var(--base-height) * 1.25);
|
||||
padding: 0 calc(var(--size600) * 1.25);
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { useApi } from '@/components/hooks';
|
||||
import { useApi } from '../useApi';
|
||||
import { useFilterParams } from '../useFilterParams';
|
||||
import { ReactQueryOptions } from '@/lib/types';
|
||||
|
||||
export function useResultQuery<T>(
|
||||
|
|
@ -6,11 +7,21 @@ export function useResultQuery<T>(
|
|||
params?: { [key: string]: any },
|
||||
options?: ReactQueryOptions<T>,
|
||||
) {
|
||||
const { websiteId } = params;
|
||||
const { post, useQuery } = useApi();
|
||||
const filterParams = useFilterParams(websiteId);
|
||||
|
||||
return useQuery<T>({
|
||||
queryKey: ['reports', type, params],
|
||||
queryFn: () => post(`/reports/${type}`, { type, ...params }),
|
||||
queryKey: [
|
||||
'reports',
|
||||
{
|
||||
type,
|
||||
websiteId,
|
||||
...filterParams,
|
||||
...params,
|
||||
},
|
||||
],
|
||||
queryFn: () => post(`/reports/${type}`, { type, ...filterParams, ...params }),
|
||||
enabled: !!type,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ export type WebsiteMetricsData = {
|
|||
|
||||
export function useWebsiteMetricsQuery(
|
||||
websiteId: string,
|
||||
queryParams: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number },
|
||||
params: { type: string; limit?: number; search?: string; startAt?: number; endAt?: number },
|
||||
options?: ReactQueryOptions<WebsiteMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
|
|
@ -24,14 +24,14 @@ export function useWebsiteMetricsQuery(
|
|||
{
|
||||
websiteId,
|
||||
...filterParams,
|
||||
...queryParams,
|
||||
...params,
|
||||
},
|
||||
],
|
||||
queryFn: async () =>
|
||||
get(`/websites/${websiteId}/metrics`, {
|
||||
...filterParams,
|
||||
[searchParams.get('view')]: undefined,
|
||||
...queryParams,
|
||||
...params,
|
||||
}),
|
||||
enabled: !!websiteId,
|
||||
placeholderData: keepPreviousData,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ export {
|
|||
Download,
|
||||
Edit,
|
||||
Ellipsis,
|
||||
Equal,
|
||||
Eye,
|
||||
ExternalLink,
|
||||
File,
|
||||
|
|
@ -20,6 +21,7 @@ export {
|
|||
KeyRound,
|
||||
LayoutDashboard,
|
||||
Link,
|
||||
ListCheck,
|
||||
ListFilter,
|
||||
LockKeyhole,
|
||||
LogOut,
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ export const reportParms = {
|
|||
endDate: z.coerce.date(),
|
||||
num: z.coerce.number().optional(),
|
||||
offset: z.coerce.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
timezone: timezoneParam.optional(),
|
||||
unit: unitParam.optional(),
|
||||
value: z.string().optional(),
|
||||
}),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ export interface QueryFilters {
|
|||
timezone?: string;
|
||||
unit?: string;
|
||||
eventType?: number;
|
||||
url?: string;
|
||||
path?: string;
|
||||
referrer?: string;
|
||||
title?: string;
|
||||
query?: string;
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ async function clickhouseQuery(
|
|||
user_activities AS (
|
||||
select distinct
|
||||
w.session_id,
|
||||
(${getDateSQL('created_at', unit)} - c.cohort_date) / 86400 as day_number
|
||||
(${getDateSQL('created_at', unit, timezone)} - c.cohort_date) / 86400 as day_number
|
||||
from website_event w
|
||||
join cohort_items c
|
||||
on w.session_id = c.session_id
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue