mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 06:37:18 +01:00
Added reports section.
This commit is contained in:
parent
ad918c5bba
commit
a5700d4a25
36 changed files with 422 additions and 43 deletions
|
|
@ -3,31 +3,22 @@ import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics';
|
|||
import { endOfYear, isSameDay } from 'date-fns';
|
||||
import DatePickerForm from 'components/metrics/DatePickerForm';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { dateFormat, getDateRangeValues } from 'lib/date';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import Icons from 'components/icons';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
|
||||
export function DateFilter({ websiteId, value, className }) {
|
||||
export function DateFilter({
|
||||
value,
|
||||
startDate,
|
||||
endDate,
|
||||
className,
|
||||
onChange,
|
||||
showAllTime = false,
|
||||
alignment = 'end',
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { get } = useApi();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
|
||||
async function handleDateChange(value) {
|
||||
if (value === 'all' && websiteId) {
|
||||
const data = await get(`/websites/${websiteId}`);
|
||||
|
||||
if (data) {
|
||||
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
||||
}
|
||||
} else if (value !== 'all') {
|
||||
setDateRange(value);
|
||||
}
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ label: formatMessage(labels.today), value: '1day' },
|
||||
{
|
||||
|
|
@ -61,7 +52,7 @@ export function DateFilter({ websiteId, value, className }) {
|
|||
value: '90day',
|
||||
},
|
||||
{ label: formatMessage(labels.thisYear), value: '1year' },
|
||||
websiteId && {
|
||||
showAllTime && {
|
||||
label: formatMessage(labels.allTime),
|
||||
value: 'all',
|
||||
divider: true,
|
||||
|
|
@ -86,12 +77,12 @@ export function DateFilter({ websiteId, value, className }) {
|
|||
setShowPicker(true);
|
||||
return;
|
||||
}
|
||||
handleDateChange(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const handlePickerChange = value => {
|
||||
setShowPicker(false);
|
||||
handleDateChange(value);
|
||||
onChange(value);
|
||||
};
|
||||
|
||||
const handleClose = () => setShowPicker(false);
|
||||
|
|
@ -103,7 +94,8 @@ export function DateFilter({ websiteId, value, className }) {
|
|||
items={options}
|
||||
renderValue={renderValue}
|
||||
value={value}
|
||||
alignment="end"
|
||||
alignment={alignment}
|
||||
placeholder={formatMessage(labels.selectDate)}
|
||||
onChange={handleChange}
|
||||
>
|
||||
{({ label, value, divider }) => (
|
||||
|
|
|
|||
26
components/input/WebsiteDateFilter.js
Normal file
26
components/input/WebsiteDateFilter.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { getDateRangeValues } from 'lib/date';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import DateFilter from './DateFilter';
|
||||
|
||||
export default function WebsiteDateFilter({ websiteId, value }) {
|
||||
const { get } = useApi();
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate } = dateRange;
|
||||
|
||||
const handleChange = async value => {
|
||||
if (value === 'all' && websiteId) {
|
||||
const data = await get(`/websites/${websiteId}`);
|
||||
|
||||
if (data) {
|
||||
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
||||
}
|
||||
} else if (value !== 'all') {
|
||||
setDateRange(value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={handleChange} />
|
||||
);
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ export function NavBar() {
|
|||
|
||||
const links = [
|
||||
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
||||
{ label: formatMessage(labels.reports), url: '/reports' },
|
||||
{ label: formatMessage(labels.realtime), url: '/realtime' },
|
||||
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
|
||||
].filter(n => n);
|
||||
|
|
|
|||
|
|
@ -97,6 +97,7 @@ export const labels = defineMessages({
|
|||
allTime: { id: 'label.all-time', defaultMessage: 'All time' },
|
||||
customRange: { id: 'label.custom-range', defaultMessage: 'Custom range' },
|
||||
selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' },
|
||||
selectDate: { id: 'label.select-date', defaultMessage: 'Select date' },
|
||||
all: { id: 'label.all', defaultMessage: 'All' },
|
||||
sessions: { id: 'label.sessions', defaultMessage: 'Sessions' },
|
||||
pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' },
|
||||
|
|
@ -117,6 +118,8 @@ export const labels = defineMessages({
|
|||
view: { id: 'label.view', defaultMessage: 'View' },
|
||||
cities: { id: 'label.cities', defaultMessage: 'Cities' },
|
||||
regions: { id: 'label.regions', defaultMessage: 'Regions' },
|
||||
reports: { id: 'label.reports', defaultMessage: 'Reports' },
|
||||
eventData: { id: 'label.event-data', defaultMessage: 'Event data' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import classNames from 'classnames';
|
|||
import PageviewsChart from './PageviewsChart';
|
||||
import MetricsBar from './MetricsBar';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import DateFilter from 'components/input/DateFilter';
|
||||
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
|
||||
import ErrorMessage from 'components/common/ErrorMessage';
|
||||
import FilterTags from 'components/metrics/FilterTags';
|
||||
import RefreshButton from 'components/input/RefreshButton';
|
||||
|
|
@ -107,7 +107,7 @@ export function WebsiteChart({
|
|||
<Column defaultSize={12} xl={4}>
|
||||
<div className={styles.actions}>
|
||||
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
|
||||
<DateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
|
||||
<WebsiteDateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
|
||||
</div>
|
||||
</Column>
|
||||
</Row>
|
||||
|
|
|
|||
33
components/pages/reports/EventDataReport.js
Normal file
33
components/pages/reports/EventDataReport.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { useState } from 'react';
|
||||
import { Form, FormRow, FormInput, TextField } from 'react-basics';
|
||||
import AppLayout from 'components/layout/AppLayout';
|
||||
import Report from './Report';
|
||||
import ReportHeader from './ReportHeader';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
import styles from './reports.module.css';
|
||||
|
||||
export default function EventDataReport({ websiteId, data }) {
|
||||
const [values, setValues] = useState({ query: '' });
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<Report>
|
||||
<ReportHeader title={formatMessage(labels.eventData)} icon={<Nodes />} />
|
||||
<div className={styles.container}>
|
||||
<div className={styles.menu}>
|
||||
<Form>
|
||||
<FormRow label="Properties">
|
||||
<FormInput name="query">
|
||||
<TextField value={values.query} />
|
||||
</FormInput>
|
||||
</FormRow>
|
||||
</Form>
|
||||
</div>
|
||||
<div className={styles.content}></div>
|
||||
</div>
|
||||
</Report>
|
||||
</AppLayout>
|
||||
);
|
||||
}
|
||||
5
components/pages/reports/Report.js
Normal file
5
components/pages/reports/Report.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import Page from 'components/layout/Page';
|
||||
|
||||
export default function Report({ children, ...props }) {
|
||||
return <Page {...props}>{children}</Page>;
|
||||
}
|
||||
42
components/pages/reports/ReportHeader.js
Normal file
42
components/pages/reports/ReportHeader.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { useState } from 'react';
|
||||
import { Flexbox, Icon, Text } from 'react-basics';
|
||||
import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import DateFilter from 'components/input/DateFilter';
|
||||
import { parseDateRange } from 'lib/date';
|
||||
|
||||
export default function ReportHeader({ title, icon }) {
|
||||
const [websiteId, setWebsiteId] = useState();
|
||||
const [dateRange, setDateRange] = useState({});
|
||||
const { value, startDate, endDate } = dateRange;
|
||||
|
||||
const handleSelect = id => {
|
||||
setWebsiteId(id);
|
||||
};
|
||||
|
||||
const handleDateChange = value => setDateRange(parseDateRange(value));
|
||||
|
||||
const Title = () => {
|
||||
return (
|
||||
<>
|
||||
<Icon size="xl">{icon}</Icon>
|
||||
<Text>{title}</Text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<PageHeader title={<Title />}>
|
||||
<Flexbox gap={20}>
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleDateChange}
|
||||
showAllTime
|
||||
/>
|
||||
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
|
||||
</Flexbox>
|
||||
</PageHeader>
|
||||
);
|
||||
}
|
||||
66
components/pages/reports/ReportsList.js
Normal file
66
components/pages/reports/ReportsList.js
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import Link from 'next/link';
|
||||
import { Button, Icons, Text, Icon } from 'react-basics';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
import Nodes from 'assets/nodes.svg';
|
||||
import Lightbulb from 'assets/lightbulb.svg';
|
||||
import styles from './ReportsList.module.css';
|
||||
|
||||
const reports = [
|
||||
{
|
||||
title: 'Event data',
|
||||
description: 'Query your event data.',
|
||||
url: '/reports/event-data',
|
||||
icon: <Nodes />,
|
||||
},
|
||||
{
|
||||
title: 'Funnel',
|
||||
description: 'Understand the conversion and drop-off rate of users.',
|
||||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: 'Insights',
|
||||
description: 'Explore your data by applying segments and filters.',
|
||||
url: '/reports/insights',
|
||||
icon: <Lightbulb />,
|
||||
},
|
||||
];
|
||||
|
||||
function Report({ title, description, url, icon }) {
|
||||
return (
|
||||
<div className={styles.report}>
|
||||
<div className={styles.title}>
|
||||
<Icon size="lg">{icon}</Icon>
|
||||
<Text>{title}</Text>
|
||||
</div>
|
||||
<div className={styles.description}>{description}</div>
|
||||
<div className={styles.buttons}>
|
||||
<Link href={url}>
|
||||
<Button variant="primary">
|
||||
<Icon>
|
||||
<Icons.Plus />
|
||||
</Icon>
|
||||
<Text>Create</Text>
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReportsList() {
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader title="Reports" />
|
||||
<div className={styles.reports}>
|
||||
{reports.map(({ title, description, url, icon }) => {
|
||||
return (
|
||||
<Report key={title} icon={icon} title={title} description={description} url={url} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
32
components/pages/reports/ReportsList.module.css
Normal file
32
components/pages/reports/ReportsList.module.css
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
.reports {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.report {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--base500);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.description {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
14
components/pages/reports/reports.module.css
Normal file
14
components/pages/reports/reports.module.css
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.container {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-columns: max-content 1fr;
|
||||
}
|
||||
|
||||
.menu {
|
||||
width: 300px;
|
||||
grid-column: 1 / 2;
|
||||
}
|
||||
|
||||
.content {
|
||||
grid-column: 2 / 3;
|
||||
}
|
||||
|
|
@ -9,11 +9,12 @@ export function DateRangeSetting() {
|
|||
const [dateRange, setDateRange] = useDateRange();
|
||||
const { startDate, endDate, value } = dateRange;
|
||||
|
||||
const handleChange = value => setDateRange(value);
|
||||
const handleReset = () => setDateRange(DEFAULT_DATE_RANGE);
|
||||
|
||||
return (
|
||||
<Flexbox gap={10}>
|
||||
<DateFilter value={value} startDate={startDate} endDate={endDate} />
|
||||
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={handleChange} />
|
||||
<Button onClick={handleReset}>{formatMessage(labels.reset)}</Button>
|
||||
</Flexbox>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue