Metrics components refactoring. New event data page.

This commit is contained in:
Mike Cao 2023-07-10 04:35:19 -07:00
parent 4e6d24e932
commit c865f43b11
47 changed files with 756 additions and 672 deletions

View file

@ -15,7 +15,6 @@ export function HamburgerButton() {
label: formatMessage(labels.dashboard),
url: '/dashboard',
},
{ label: formatMessage(labels.realtime), url: '/realtime' },
!cloudMode && {
label: formatMessage(labels.settings),
url: '/settings',

View file

@ -1,11 +1,12 @@
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import DateFilter from './DateFilter';
import styles from './WebsiteDateFilter.module.css';
export default function WebsiteDateFilter({ websiteId, value }) {
export default function WebsiteDateFilter({ websiteId }) {
const { get } = useApi();
const [dateRange, setDateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const { value, startDate, endDate } = dateRange;
const handleChange = async value => {
if (value === 'all' && websiteId) {
@ -20,6 +21,12 @@ export default function WebsiteDateFilter({ websiteId, value }) {
};
return (
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={handleChange} />
<DateFilter
className={styles.dropdown}
value={value}
startDate={startDate}
endDate={endDate}
onChange={handleChange}
/>
);
}

View file

@ -0,0 +1,3 @@
.dropdown {
min-width: 200px;
}

View file

@ -18,8 +18,6 @@ export function NavBar() {
const links = [
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
{ label: formatMessage(labels.realtime), url: '/realtime' },
{ label: formatMessage(labels.reports), url: '/reports' },
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
].filter(n => n);

View file

@ -1,114 +1,13 @@
import { useState } from 'react';
import { Loading } from 'react-basics';
import ErrorMessage from 'components/common/ErrorMessage';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import usePageQuery from 'hooks/usePageQuery';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard';
import useMessages from 'hooks/useMessages';
import styles from './MetricsBar.module.css';
export function MetricsBar({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true);
const {
query: { url, referrer, title, os, browser, device, country, region, city },
} = usePageQuery();
const { data, error, isLoading, isFetched } = useQuery(
[
'websites:stats',
{ websiteId, modified, url, referrer, title, os, browser, device, country, region, city },
],
() =>
get(`/websites/${websiteId}/stats`, {
startAt: +startDate,
endAt: +endDate,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
}),
);
const formatFunc = format
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
: formatNumber;
function handleSetFormat() {
setFormat(state => !state);
}
const { pageviews, uniques, bounces, totaltime } = data || {};
const num = Math.min(data && uniques.value, data && bounces.value);
const diffs = data && {
pageviews: pageviews.value - pageviews.change,
uniques: uniques.value - uniques.change,
bounces: bounces.value - bounces.change,
totaltime: totaltime.value - totaltime.change,
};
export function MetricsBar({ children, onClick, isLoading, isFetched, error }) {
return (
<div className={styles.bar} onClick={handleSetFormat}>
<div className={styles.bar} onClick={onClick}>
{isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{data && !error && isFetched && (
<>
<MetricCard
className={styles.card}
label={formatMessage(labels.views)}
value={pageviews.value}
change={pageviews.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.visitors)}
value={uniques.value}
change={uniques.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.bounceRate)}
value={uniques.value ? (num / uniques.value) * 100 : 0}
change={
uniques.value && uniques.change
? (num / uniques.value) * 100 -
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
: 0
}
format={n => Number(n).toFixed(0) + '%'}
reverseColors
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.averageVisitTime)}
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)
: 0
}
change={
totaltime.value && pageviews.value
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
totaltime.value / (pageviews.value - bounces.value)) *
-1 || 0
: 0
}
format={n => `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`}
/>
</>
)}
{children}
</div>
);
}

View file

@ -1,5 +1,6 @@
.bar {
display: flex;
flex-direction: row;
cursor: pointer;
min-height: 110px;
gap: 20px;

View file

@ -1,132 +0,0 @@
import { useMemo } from 'react';
import { Button, Icon, Text, Row, Column } from 'react-basics';
import Link from 'next/link';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import ErrorMessage from 'components/common/ErrorMessage';
import FilterTags from 'components/metrics/FilterTags';
import RefreshButton from 'components/input/RefreshButton';
import useApi from 'hooks/useApi';
import useDateRange from 'hooks/useDateRange';
import useTimezone from 'hooks/useTimezone';
import usePageQuery from 'hooks/usePageQuery';
import { getDateArray, getDateLength } from 'lib/date';
import Icons from 'components/icons';
import useSticky from 'hooks/useSticky';
import useMessages from 'hooks/useMessages';
import styles from './WebsiteChart.module.css';
import useLocale from 'hooks/useLocale';
export function WebsiteChart({
websiteId,
name,
domain,
stickyHeader = false,
showChart = true,
showDetailsButton = false,
onDataLoad = () => {},
}) {
const { formatMessage, labels } = useMessages();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, value, modified } = dateRange;
const [timezone] = useTimezone();
const {
query: { url, referrer, os, browser, device, country, region, city, title },
} = usePageQuery();
const { get, useQuery } = useApi();
const { ref, isSticky } = useSticky({ enabled: stickyHeader });
const { data, isLoading, error } = useQuery(
[
'websites:pageviews',
{ websiteId, modified, url, referrer, os, browser, device, country, region, city, title },
],
() =>
get(`/websites/${websiteId}/pageviews`, {
startAt: +startDate,
endAt: +endDate,
unit,
timezone,
url,
referrer,
os,
browser,
device,
country,
region,
city,
title,
}),
{ onSuccess: onDataLoad },
);
const chartData = useMemo(() => {
if (data) {
return {
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
sessions: getDateArray(data.sessions, startDate, endDate, unit),
};
}
return { pageviews: [], sessions: [] };
}, [data, modified]);
const { dir } = useLocale();
return (
<>
<WebsiteHeader websiteId={websiteId} name={name} domain={domain}>
{showDetailsButton && (
<Link href={`/websites/${websiteId}`}>
<Button variant="primary">
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight />
</Icon>
</Icon>
</Button>
</Link>
)}
</WebsiteHeader>
<FilterTags
websiteId={websiteId}
params={{ url, referrer, os, browser, device, country, region, city, title }}
/>
<Row
ref={ref}
className={classNames(styles.header, {
[styles.sticky]: stickyHeader,
[styles.isSticky]: isSticky,
})}
>
<Column defaultSize={12} xl={8}>
<MetricsBar websiteId={websiteId} />
</Column>
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
<RefreshButton websiteId={websiteId} isLoading={isLoading} />
<WebsiteDateFilter websiteId={websiteId} value={value} className={styles.dropdown} />
</div>
</Column>
</Row>
<Row>
<Column className={styles.chart}>
{error && <ErrorMessage />}
{showChart && (
<PageviewsChart
websiteId={websiteId}
data={chartData}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
loading={isLoading}
/>
)}
</Column>
</Row>
</>
);
}
export default WebsiteChart;

View file

@ -1,42 +0,0 @@
import { Flexbox, Row, Column, Text, Button, Icon } from 'react-basics';
import Favicon from 'components/common/Favicon';
import ActiveUsers from './ActiveUsers';
import styles from './WebsiteHeader.module.css';
import { useMessages } from 'hooks';
import Icons from 'components/icons';
export function WebsiteHeader({ websiteId, name, domain, children }) {
const { formatMessage, labels } = useMessages();
const links = [
{ label: formatMessage(labels.overview), icon: <Icons.Overview /> },
{ label: formatMessage(labels.realtime), icon: <Icons.Clock /> },
{ label: formatMessage(labels.reports), icon: <Icons.Reports /> },
{ label: formatMessage(labels.eventData), icon: <Icons.Nodes /> },
];
return (
<Row className={styles.header} justifyContent="center">
<Column className={styles.title} variant="two">
<Favicon domain={domain} />
<Text>{name}</Text>
</Column>
<Column className={styles.actions} variant="two">
<ActiveUsers websiteId={websiteId} />
<Flexbox alignItems="center">
{links.map(({ label, icon }) => {
return (
<Button key={label} variant="quiet">
<Icon>{icon}</Icon>
<Text>{label}</Text>
</Button>
);
})}
</Flexbox>
{children}
</Column>
</Row>
);
}
export default WebsiteHeader;

View file

@ -2,7 +2,7 @@ import WebsiteSelect from 'components/input/WebsiteSelect';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import EventsChart from 'components/metrics/EventsChart';
import WebsiteChart from 'components/metrics/WebsiteChart';
import WebsiteChart from 'components/pages/websites/WebsiteChart';
import useApi from 'hooks/useApi';
import Head from 'next/head';
import Link from 'next/link';
@ -143,12 +143,7 @@ export function TestConsole() {
</Row>
<Row>
<Column>
<WebsiteChart
websiteId={website.id}
name={website.name}
domain={website.domain}
showLink
/>
<WebsiteChart websiteId={website.id} />
<EventsChart websiteId={website.id} />
</Column>
</Row>

View file

@ -0,0 +1,47 @@
import { Column, Row } from 'react-basics';
import { useApi, useDateRange } from 'hooks';
import MetricCard from 'components/metrics/MetricCard';
import useMessages from 'hooks/useMessages';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import MetricsBar from 'components/metrics/MetricsBar';
import styles from './EventDataMetricsBar.module.css';
export function EventDataMetricsBar({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const { data, error, isLoading, isFetched } = useQuery(
['event-data:fields', { websiteId, startDate, endDate, modified }],
() =>
get(`/event-data/fields`, {
websiteId,
startAt: +startDate,
endAt: +endDate,
}),
);
return (
<Row>
<Column defaultSize={12} xl={8}>
<MetricsBar isLoading={isLoading} isFetched={isFetched} error={error}>
{!error && isFetched && (
<MetricCard
className={styles.card}
label={formatMessage(labels.fields)}
value={data?.length}
/>
)}
</MetricsBar>
</Column>
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
<WebsiteDateFilter websiteId={websiteId} />
</div>
</Column>
</Row>
);
}
export default EventDataMetricsBar;

View file

@ -0,0 +1,42 @@
.container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
min-height: 90px;
margin-bottom: 20px;
background: var(--base50);
z-index: var(--z-index-above);
}
.metrics {
display: flex;
flex-direction: row;
align-items: center;
}
.actions {
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-end;
flex: 1;
}
.bar {
display: flex;
cursor: pointer;
min-height: 110px;
gap: 20px;
flex-wrap: wrap;
}
.card {
justify-self: flex-start;
}
@media only screen and (max-width: 992px) {
.card {
flex-basis: calc(50% - 20px);
}
}

View file

@ -0,0 +1,18 @@
import { GridTable, GridColumn } from 'react-basics';
import { useMessages } from 'hooks';
export function EventDataTable({ data = [], showValue }) {
const { formatMessage, labels } = useMessages();
return (
<GridTable data={data}>
<GridColumn name="field" label={formatMessage(labels.field)} />
<GridColumn name="value" label={formatMessage(labels.value)} hidden={!showValue} />
<GridColumn name="total" label={formatMessage(labels.total)}>
{({ total }) => total.toLocaleString()}
</GridColumn>
</GridTable>
);
}
export default EventDataTable;

View file

@ -1,22 +1,20 @@
import { useState, useEffect, useMemo } from 'react';
import { subMinutes, startOfMinute } from 'date-fns';
import { useRouter } from 'next/router';
import firstBy from 'thenby';
import { GridRow, GridColumn } from 'components/layout/Grid';
import Page from 'components/layout/Page';
import RealtimeChart from 'components/metrics/RealtimeChart';
import PageHeader from 'components/layout/PageHeader';
import WorldMap from 'components/common/WorldMap';
import RealtimeLog from 'components/pages/realtime/RealtimeLog';
import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
import RealtimeCountries from 'components/pages/realtime/RealtimeCountries';
import WebsiteSelect from 'components/input/WebsiteSelect';
import WebsiteHeader from 'components/pages/websites/WebsiteHeader';
import useApi from 'hooks/useApi';
import useMessages from 'hooks/useMessages';
import { percentFilter } from 'lib/filters';
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
import styles from './RealtimeDashboard.module.css';
import styles from './RealtimePage.module.css';
import { useWebsite } from 'hooks';
function mergeData(state = [], data = [], time) {
const ids = state.map(({ __id }) => __id);
@ -25,12 +23,10 @@ function mergeData(state = [], data = [], time) {
.filter(({ timestamp }) => timestamp >= time);
}
export function RealtimeDashboard({ websiteId }) {
const { formatMessage, labels } = useMessages();
const router = useRouter();
export function RealtimePage({ websiteId }) {
const [currentData, setCurrentData] = useState();
const { get, useQuery } = useApi();
const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`));
const { data: website } = useWebsite(websiteId);
const { data, isLoading, error } = useQuery(
['realtime', websiteId],
() => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
@ -93,15 +89,9 @@ export function RealtimeDashboard({ websiteId }) {
return currentData;
}, [currentData]);
const handleSelect = id => {
router.push(`/realtime/${id}`);
};
return (
<Page loading={isLoading} error={error}>
<PageHeader title={formatMessage(labels.realtime)}>
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
</PageHeader>
<WebsiteHeader websiteId={websiteId} />
<RealtimeHeader websiteId={websiteId} data={currentData} />
<div className={styles.chart}>
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
@ -126,4 +116,4 @@ export function RealtimeDashboard({ websiteId }) {
);
}
export default RealtimeDashboard;
export default RealtimePage;

View file

@ -5,13 +5,13 @@ import { Button, Icon, Icons, Text } from 'react-basics';
import { useMessages, useReports } from 'hooks';
import ReportsTable from './ReportsTable';
export function ReportsList() {
export function ReportsPage() {
const { formatMessage, labels } = useMessages();
const { reports, error, isLoading } = useReports();
return (
<Page loading={isLoading} error={error}>
<PageHeader title="Reports">
<PageHeader title={formatMessage(labels.reports)}>
<Link href="/reports/create">
<Button variant="primary">
<Icon>
@ -26,4 +26,4 @@ export function ReportsList() {
);
}
export default ReportsList;
export default ReportsPage;

View file

@ -0,0 +1,59 @@
import { useMemo } from 'react';
import PageviewsChart from 'components/metrics/PageviewsChart';
import { useApi, useDateRange, useTimezone, usePageQuery } from 'hooks';
import { getDateArray, getDateLength } from 'lib/date';
export function WebsiteChart({ websiteId }) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, unit, modified } = dateRange;
const [timezone] = useTimezone();
const {
query: { url, referrer, os, browser, device, country, region, city, title },
} = usePageQuery();
const { get, useQuery } = useApi();
const { data, isLoading } = useQuery(
[
'websites:pageviews',
{ websiteId, modified, url, referrer, os, browser, device, country, region, city, title },
],
() =>
get(`/websites/${websiteId}/pageviews`, {
startAt: +startDate,
endAt: +endDate,
unit,
timezone,
url,
referrer,
os,
browser,
device,
country,
region,
city,
title,
}),
);
const chartData = useMemo(() => {
if (data) {
return {
pageviews: getDateArray(data.pageviews, startDate, endDate, unit),
sessions: getDateArray(data.sessions, startDate, endDate, unit),
};
}
return { pageviews: [], sessions: [] };
}, [data, startDate, endDate, unit, modified]);
return (
<PageviewsChart
websiteId={websiteId}
data={chartData}
unit={unit}
records={getDateLength(startDate, endDate, unit)}
loading={isLoading}
/>
);
}
export default WebsiteChart;

View file

@ -0,0 +1,17 @@
.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;
}

View file

@ -1,11 +1,19 @@
import { Button, Text, Icon } from 'react-basics';
import { useMemo } from 'react';
import { firstBy } from 'thenby';
import WebsiteChart from 'components/metrics/WebsiteChart';
import Link from 'next/link';
import WebsiteChart from 'components/pages/websites/WebsiteChart';
import useDashboard from 'store/dashboard';
import styles from './WebsiteList.module.css';
import WebsiteHeader from './WebsiteHeader';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
import { useMessages, useLocale } from 'hooks';
import Icons from 'components/icons';
export default function WebsiteChartList({ websites, showCharts, limit }) {
const { formatMessage, labels } = useMessages();
const { websiteOrder } = useDashboard();
const { dir } = useLocale();
const ordered = useMemo(
() =>
@ -17,16 +25,23 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
return (
<div>
{ordered.map(({ id, name, domain }, index) => {
{ordered.map(({ id }, index) => {
return index < limit ? (
<div key={id} className={styles.website}>
<WebsiteChart
websiteId={id}
name={name}
domain={domain}
showChart={showCharts}
showDetailsButton={true}
/>
<WebsiteHeader websiteId={id} showLinks={false}>
<Link href={`/websites/${id}`}>
<Button variant="primary">
<Text>{formatMessage(labels.viewDetails)}</Text>
<Icon>
<Icon rotate={dir === 'rtl' ? 180 : 0}>
<Icons.ArrowRight />
</Icon>
</Icon>
</Button>
</Link>
</WebsiteHeader>
<WebsiteMetricsBar websiteId={id} />
<WebsiteChart websiteId={id} showChart={showCharts} />
</div>
) : null;
})}

View file

@ -1,47 +0,0 @@
import { useState } from 'react';
import { Loading } from 'react-basics';
import Page from 'components/layout/Page';
import WebsiteChart from 'components/metrics/WebsiteChart';
import useApi from 'hooks/useApi';
import usePageQuery from 'hooks/usePageQuery';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import WebsiteTableView from './WebsiteTableView';
import WebsiteMenuView from './WebsiteMenuView';
export default function WebsiteDetails({ websiteId }) {
const { get, useQuery } = useApi();
const { data, isLoading, error } = useQuery(['websites', websiteId], () =>
get(`/websites/${websiteId}`),
);
const [chartLoaded, setChartLoaded] = useState(false);
const {
query: { view },
} = usePageQuery();
function handleDataLoad() {
if (!chartLoaded) {
setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION);
}
}
return (
<Page loading={isLoading} error={error}>
<WebsiteChart
websiteId={websiteId}
name={data?.name}
domain={data?.domain}
onDataLoad={handleDataLoad}
showLink={false}
stickyHeader={true}
/>
{!chartLoaded && <Loading icon="dots" style={{ minHeight: 300 }} />}
{chartLoaded && (
<>
{!view && <WebsiteTableView websiteId={websiteId} />}
{view && <WebsiteMenuView websiteId={websiteId} />}
</>
)}
</Page>
);
}

View file

@ -1,31 +0,0 @@
.chart {
margin-bottom: 30px;
}
.view {
border-top: 1px solid var(--base300);
}
.menu {
font-size: var(--font-size-sm);
}
.content {
min-height: 600px;
padding: 20px 0;
}
.backButton {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 16px;
}
.backButton svg {
transform: rotate(180deg);
}
.hidden {
display: none;
}

View file

@ -0,0 +1,37 @@
import { Loading } from 'react-basics';
import Page from 'components/layout/Page';
import WebsiteChart from 'components/pages/websites/WebsiteChart';
import FilterTags from 'components/metrics/FilterTags';
import usePageQuery from 'hooks/usePageQuery';
import WebsiteTableView from './WebsiteTableView';
import WebsiteMenuView from './WebsiteMenuView';
import { useWebsite } from 'hooks';
import WebsiteHeader from './WebsiteHeader';
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
export default function WebsiteDetailsPage({ websiteId }) {
const { data: website, isLoading, error } = useWebsite(websiteId);
const {
query: { view, url, referrer, os, browser, device, country, region, city, title },
} = usePageQuery();
return (
<Page loading={isLoading} error={error}>
<WebsiteHeader websiteId={websiteId} />
<WebsiteMetricsBar websiteId={websiteId} sticky={true} />
<WebsiteChart websiteId={websiteId} />
<FilterTags
websiteId={websiteId}
params={{ url, referrer, os, browser, device, country, region, city, title }}
/>
{!website && <Loading icon="dots" style={{ minHeight: 300 }} />}
{website && (
<>
{!view && <WebsiteTableView websiteId={websiteId} />}
{view && <WebsiteMenuView websiteId={websiteId} />}
</>
)}
</Page>
);
}

View file

@ -0,0 +1,35 @@
import EventDataTable from 'components/pages/event-data/EventDataTable';
import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar';
import { useDateRange, useApi, usePageQuery } from 'hooks';
import styles from './WebsiteEventData.module.css';
function useFields(websiteId, field) {
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate } = dateRange;
const { get, useQuery } = useApi();
const { data, error, isLoading } = useQuery(
['event-data:fields', websiteId, startDate, endDate],
() =>
get('/event-data', {
websiteId,
startAt: +startDate,
endAt: +endDate,
field,
}),
{ enabled: !!(websiteId && startDate && endDate) },
);
return { data, error, isLoading };
}
export default function WebsiteEventData({ websiteId }) {
const { data } = useFields(websiteId);
const { query } = usePageQuery();
return (
<div className={styles.container}>
<EventDataMetricsBar websiteId={websiteId} />
<EventDataTable data={data} showValue={query?.field} />
</div>
);
}

View file

@ -0,0 +1,9 @@
.container {
display: flex;
flex-direction: column;
}
.header {
display: flex;
justify-content: flex-end;
}

View file

@ -0,0 +1,12 @@
import Page from 'components/layout/Page';
import WebsiteHeader from './WebsiteHeader';
import WebsiteEventData from './WebsiteEventData';
export default function WebsiteEventDataPage({ websiteId }) {
return (
<Page>
<WebsiteHeader websiteId={websiteId} />
<WebsiteEventData websiteId={websiteId} />
</Page>
);
}

View file

@ -0,0 +1,78 @@
import classNames from 'classnames';
import { Flexbox, Row, Column, Text, Button, Icon } from 'react-basics';
import Link from 'next/link';
import { useRouter } from 'next/router';
import Favicon from 'components/common/Favicon';
import ActiveUsers from 'components/metrics/ActiveUsers';
import styles from './WebsiteHeader.module.css';
import Icons from 'components/icons';
import { useMessages, useWebsite } from 'hooks';
export function WebsiteHeader({ websiteId, showLinks = true, children }) {
const { formatMessage, labels } = useMessages();
const { asPath, pathname } = useRouter();
const { data: website } = useWebsite(websiteId);
const { name, domain } = website || {};
const links = [
{
label: formatMessage(labels.overview),
icon: <Icons.Overview />,
path: '',
},
{
label: formatMessage(labels.realtime),
icon: <Icons.Clock />,
path: '/realtime',
},
{
label: formatMessage(labels.reports),
icon: <Icons.Reports />,
path: '/reports',
},
{
label: formatMessage(labels.eventData),
icon: <Icons.Nodes />,
path: '/event-data',
},
];
return (
<Row className={styles.header} justifyContent="center">
<Column className={styles.title} variant="two">
<Favicon domain={domain} />
<Text>{name}</Text>
</Column>
<Column className={styles.actions} variant="two">
<ActiveUsers websiteId={websiteId} />
{showLinks && (
<Flexbox alignItems="center">
{links.map(({ label, icon, path }) => {
const query = path.indexOf('?');
const selected = path
? asPath.endsWith(query >= 0 ? path.substring(0, query) : path)
: pathname === '/websites/[id]';
return (
<Link key={label} href={`/websites/${websiteId}${path}`} shallow={true}>
<Button
variant="quiet"
className={classNames({
[styles.selected]: selected,
})}
>
<Icon>{icon}</Icon>
<Text>{label}</Text>
</Button>
</Link>
);
})}
</Flexbox>
)}
{children}
</Column>
</Row>
);
}
export default WebsiteHeader;

View file

@ -23,3 +23,7 @@
gap: 30px;
min-height: 0;
}
.selected {
font-weight: bold;
}

View file

@ -0,0 +1,138 @@
import { useState } from 'react';
import classNames from 'classnames';
import { Row, Column } from 'react-basics';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from 'components/metrics/MetricCard';
import RefreshButton from 'components/input/RefreshButton';
import WebsiteDateFilter from 'components/input/WebsiteDateFilter';
import MetricsBar from 'components/metrics/MetricsBar';
import { useApi, useDateRange, usePageQuery, useMessages, useSticky } from 'hooks';
import styles from './WebsiteMetricsBar.module.css';
export function WebsiteMetricsBar({ websiteId, sticky }) {
const { formatMessage, labels } = useMessages();
const { get, useQuery } = useApi();
const [dateRange] = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const [format, setFormat] = useState(true);
const { ref, isSticky } = useSticky({ enabled: sticky });
const {
query: { url, referrer, title, os, browser, device, country, region, city },
} = usePageQuery();
const { data, error, isLoading, isFetched } = useQuery(
[
'websites:stats',
{ websiteId, modified, url, referrer, title, os, browser, device, country, region, city },
],
() =>
get(`/websites/${websiteId}/stats`, {
startAt: +startDate,
endAt: +endDate,
url,
referrer,
title,
os,
browser,
device,
country,
region,
city,
}),
);
const formatFunc = format
? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`)
: formatNumber;
function handleSetFormat() {
setFormat(state => !state);
}
const { pageviews, uniques, bounces, totaltime } = data || {};
const num = Math.min(data && uniques.value, data && bounces.value);
const diffs = data && {
pageviews: pageviews.value - pageviews.change,
uniques: uniques.value - uniques.change,
bounces: bounces.value - bounces.change,
totaltime: totaltime.value - totaltime.change,
};
return (
<Row
ref={ref}
className={classNames(styles.container, {
[styles.sticky]: sticky,
[styles.isSticky]: isSticky,
})}
>
<Column defaultSize={12} xl={8}>
<MetricsBar
isLoading={isLoading}
isFetched={isFetched}
error={error}
onClick={handleSetFormat}
>
{!error && isFetched && (
<>
<MetricCard
className={styles.card}
label={formatMessage(labels.views)}
value={pageviews.value}
change={pageviews.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.visitors)}
value={uniques.value}
change={uniques.change}
format={formatFunc}
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.bounceRate)}
value={uniques.value ? (num / uniques.value) * 100 : 0}
change={
uniques.value && uniques.change
? (num / uniques.value) * 100 -
(Math.min(diffs.uniques, diffs.bounces) / diffs.uniques) * 100 || 0
: 0
}
format={n => Number(n).toFixed(0) + '%'}
reverseColors
/>
<MetricCard
className={styles.card}
label={formatMessage(labels.averageVisitTime)}
value={
totaltime.value && pageviews.value
? totaltime.value / (pageviews.value - bounces.value)
: 0
}
change={
totaltime.value && pageviews.value
? (diffs.totaltime / (diffs.pageviews - diffs.bounces) -
totaltime.value / (pageviews.value - bounces.value)) *
-1 || 0
: 0
}
format={n =>
`${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`
}
/>
</>
)}
</MetricsBar>
</Column>
<Column defaultSize={12} xl={4}>
<div className={styles.actions}>
<RefreshButton websiteId={websiteId} />
<WebsiteDateFilter websiteId={websiteId} />
</div>
</Column>
</Row>
);
}
export default WebsiteMetricsBar;

View file

@ -1,22 +1,4 @@
.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;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
@ -35,8 +17,10 @@
gap: 10px;
}
.dropdown {
min-width: 200px;
@media only screen and (max-width: 1200px) {
.actions {
margin-top: 40px;
}
}
@media only screen and (min-width: 992px) {
@ -49,9 +33,3 @@
border-bottom: 1px solid var(--base300);
}
}
@media only screen and (max-width: 1200px) {
.actions {
margin-top: 40px;
}
}

View file

@ -0,0 +1,30 @@
import Page from 'components/layout/Page';
import Link from 'next/link';
import { Button, Icon, Icons, Text, Flexbox } from 'react-basics';
import { useMessages, useReports } from 'hooks';
import ReportsTable from 'components/pages/reports/ReportsTable';
import WebsiteHeader from './WebsiteHeader';
export function WebsiteReportsPage({ websiteId }) {
const { formatMessage, labels } = useMessages();
const { reports, error, isLoading } = useReports(websiteId);
return (
<Page loading={isLoading} error={error}>
<WebsiteHeader websiteId={websiteId} />
<Flexbox alignItems="center" justifyContent="end">
<Link href="/reports/create">
<Button variant="primary">
<Icon>
<Icons.Plus />
</Icon>
<Text>{formatMessage(labels.createReport)}</Text>
</Button>
</Link>
</Flexbox>
<ReportsTable websiteId={websiteId} data={reports} />
</Page>
);
}
export default WebsiteReportsPage;