Merge remote-tracking branch 'origin/dev' into dev
1
assets/expand.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill-rule="evenodd" stroke-linejoin="round" stroke-miterlimit="2" clip-rule="evenodd" viewBox="0 0 48 48"><path d="M7.5 40.018v-10.5c0-1.379-1.12-2.5-2.5-2.5s-2.5 1.121-2.5 2.5v11a4.5 4.5 0 0 0 4.5 4.5h12a2.5 2.5 0 0 0 0-5zm33 0H29a2.5 2.5 0 0 0 0 5h12a4.5 4.5 0 0 0 4.5-4.5v-11c0-1.379-1.12-2.5-2.5-2.5s-2.5 1.121-2.5 2.5zm-33-33H19a2.5 2.5 0 0 0 0-5H7a4.5 4.5 0 0 0-4.5 4.5v11a2.5 2.5 0 0 0 5 0zm33 0v10.5a2.5 2.5 0 0 0 5 0v-11a4.5 4.5 0 0 0-4.5-4.5H29a2.5 2.5 0 0 0 0 5z"/></svg>
|
||||||
|
After Width: | Height: | Size: 547 B |
|
|
@ -1 +1 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 512 512" viewBox="0 0 512 512"><path d="M223.718 124.76c-48.027 11.198-86.688 49.285-98.494 97.031-11.843 47.899 1.711 96.722 36.259 130.601C173.703 364.377 181 383.586 181 403.777V407c0 13.296 5.801 25.26 15 33.505V467c0 24.813 20.187 45 45 45h30c24.813 0 45-20.187 45-45v-26.495c9.199-8.245 15-20.208 15-33.505v-3.282c0-19.884 7.687-39.458 20.563-52.361C376.994 325.87 391 292.005 391 256c0-86.079-79.769-151.638-167.282-131.24zM286 467c0 8.271-6.729 15-15 15h-30c-8.271 0-15-6.729-15-15v-15h60v15zm44.326-136.834C311.689 348.843 301 375.651 301 403.718V407c0 8.271-6.729 15-15 15h-60c-8.271 0-15-6.729-15-15v-3.223c0-28.499-10.393-55.035-28.513-72.804-26.89-26.37-37.409-64.493-28.141-101.981 9.125-36.907 39.029-66.353 76.184-75.015C299.202 137.964 361 189.228 361 256c0 28.004-10.894 54.343-30.674 74.166zM139.327 118.114 96.9 75.688c-5.857-5.858-15.355-5.858-21.213 0-5.858 5.858-5.858 15.355 0 21.213l42.427 42.426c5.857 5.858 15.356 5.858 21.213 0 5.858-5.858 5.858-15.355 0-21.213zM76 241H15c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zM497 241h-61c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zM436.313 75.688c-5.856-5.858-15.354-5.858-21.213 0l-42.427 42.426c-5.858 5.857-5.858 15.355 0 21.213 5.857 5.858 15.355 5.858 21.213 0l42.427-42.426c5.858-5.857 5.858-15.355 0-21.213zM256 0c-8.284 0-15 6.716-15 15v61c0 8.284 6.716 15 15 15s15-6.716 15-15V15c0-8.284-6.716-15-15-15z"/><path d="M256 181c-6.166 0-12.447.739-18.658 2.194-25.865 6.037-47.518 27.328-53.879 52.979-1.994 8.041 2.907 16.175 10.947 18.17 8.042 1.994 16.176-2.909 18.17-10.948 3.661-14.758 16.647-27.5 31.593-30.989 3.982-.933 7.962-1.406 11.827-1.406 8.284 0 15-6.716 15-15s-6.716-15-15-15z"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" style="enable-background:new 0 0 512 512" viewBox="0 0 512 512"><path d="M223.718 124.76c-48.027 11.198-86.688 49.285-98.494 97.031-11.843 47.899 1.711 96.722 36.259 130.601C173.703 364.377 181 383.586 181 403.777V407c0 13.296 5.801 25.26 15 33.505V467c0 24.813 20.187 45 45 45h30c24.813 0 45-20.187 45-45v-26.495c9.199-8.245 15-20.208 15-33.505v-3.282c0-19.884 7.687-39.458 20.563-52.361C376.994 325.87 391 292.005 391 256c0-86.079-79.769-151.638-167.282-131.24zM286 467c0 8.271-6.729 15-15 15h-30c-8.271 0-15-6.729-15-15v-15h60v15zm44.326-136.834C311.689 348.843 301 375.651 301 403.718V407c0 8.271-6.729 15-15 15h-60c-8.271 0-15-6.729-15-15v-3.223c0-28.499-10.393-55.035-28.513-72.804-26.89-26.37-37.409-64.493-28.141-101.981 9.125-36.907 39.029-66.353 76.184-75.015C299.202 137.964 361 189.228 361 256c0 28.004-10.894 54.343-30.674 74.166zM139.327 118.114 96.9 75.688c-5.857-5.858-15.355-5.858-21.213 0-5.858 5.858-5.858 15.355 0 21.213l42.427 42.426c5.857 5.858 15.356 5.858 21.213 0 5.858-5.858 5.858-15.355 0-21.213zM76 241H15c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zm421 0h-61c-8.284 0-15 6.716-15 15s6.716 15 15 15h61c8.284 0 15-6.716 15-15s-6.716-15-15-15zM436.313 75.688c-5.856-5.858-15.354-5.858-21.213 0l-42.427 42.426c-5.858 5.857-5.858 15.355 0 21.213 5.857 5.858 15.355 5.858 21.213 0l42.427-42.426c5.858-5.857 5.858-15.355 0-21.213zM256 0c-8.284 0-15 6.716-15 15v61c0 8.284 6.716 15 15 15s15-6.716 15-15V15c0-8.284-6.716-15-15-15z"/><path d="M256 181c-6.166 0-12.447.739-18.658 2.194-25.865 6.037-47.518 27.328-53.879 52.979-1.994 8.041 2.907 16.175 10.947 18.17 8.042 1.994 16.176-2.909 18.17-10.948 3.661-14.758 16.647-27.5 31.593-30.989 3.982-.933 7.962-1.406 11.827-1.406 8.284 0 15-6.716 15-15s-6.716-15-15-15z"/></svg>
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
1
assets/overview.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" viewBox="0 0 512 512"><path d="M452 36H60C26.916 36 0 62.916 0 96v240c0 33.084 26.916 60 60 60h176v40H132v40h248v-40H276v-40h176c33.084 0 60-26.916 60-60V96c0-33.084-26.916-60-60-60zm20 300c0 11.028-8.972 20-20 20H60c-11.028 0-20-8.972-20-20V96c0-11.028 8.972-20 20-20h392c11.028 0 20 8.972 20 20v240z"/></svg>
|
||||||
|
After Width: | Height: | Size: 371 B |
1
assets/reports.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="M61.17 18.91A32 32 0 1 0 46.4 60.54l.15-.06.16-.1a31.93 31.93 0 0 0 14.47-41.44s-.01-.02-.01-.03zm-4.53-.16L34 28.91V4.1a28 28 0 0 1 22.64 14.65zM4 32A28 28 0 0 1 30 4.1V32a1.74 1.74 0 0 0 0 .39.17.17 0 0 0 0 .07 1.49 1.49 0 0 0 .15.4l12.76 24.9A28 28 0 0 1 4 32zm42.47 23.94L34.74 33l23.54-10.6a28 28 0 0 1-11.81 33.54z"/></svg>
|
||||||
|
After Width: | Height: | Size: 398 B |
|
|
@ -15,7 +15,6 @@ export function HamburgerButton() {
|
||||||
label: formatMessage(labels.dashboard),
|
label: formatMessage(labels.dashboard),
|
||||||
url: '/dashboard',
|
url: '/dashboard',
|
||||||
},
|
},
|
||||||
{ label: formatMessage(labels.realtime), url: '/realtime' },
|
|
||||||
!cloudMode && {
|
!cloudMode && {
|
||||||
label: formatMessage(labels.settings),
|
label: formatMessage(labels.settings),
|
||||||
url: '/settings',
|
url: '/settings',
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,10 @@ import Globe from 'assets/globe.svg';
|
||||||
import Lock from 'assets/lock.svg';
|
import Lock from 'assets/lock.svg';
|
||||||
import Logo from 'assets/logo.svg';
|
import Logo from 'assets/logo.svg';
|
||||||
import Moon from 'assets/moon.svg';
|
import Moon from 'assets/moon.svg';
|
||||||
|
import Nodes from 'assets/nodes.svg';
|
||||||
|
import Overview from 'assets/overview.svg';
|
||||||
import Profile from 'assets/profile.svg';
|
import Profile from 'assets/profile.svg';
|
||||||
|
import Reports from 'assets/reports.svg';
|
||||||
import Sun from 'assets/sun.svg';
|
import Sun from 'assets/sun.svg';
|
||||||
import User from 'assets/user.svg';
|
import User from 'assets/user.svg';
|
||||||
import Users from 'assets/users.svg';
|
import Users from 'assets/users.svg';
|
||||||
|
|
@ -29,7 +32,10 @@ const icons = {
|
||||||
Lock,
|
Lock,
|
||||||
Logo,
|
Logo,
|
||||||
Moon,
|
Moon,
|
||||||
|
Nodes,
|
||||||
|
Overview,
|
||||||
Profile,
|
Profile,
|
||||||
|
Reports,
|
||||||
Sun,
|
Sun,
|
||||||
User,
|
User,
|
||||||
Users,
|
Users,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import useDateRange from 'hooks/useDateRange';
|
import useDateRange from 'hooks/useDateRange';
|
||||||
import DateFilter from './DateFilter';
|
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 { get } = useApi();
|
||||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||||
const { startDate, endDate } = dateRange;
|
const { value, startDate, endDate } = dateRange;
|
||||||
|
|
||||||
const handleChange = async value => {
|
const handleChange = async value => {
|
||||||
if (value === 'all' && websiteId) {
|
if (value === 'all' && websiteId) {
|
||||||
|
|
@ -20,6 +21,12 @@ export default function WebsiteDateFilter({ websiteId, value }) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DateFilter value={value} startDate={startDate} endDate={endDate} onChange={handleChange} />
|
<DateFilter
|
||||||
|
className={styles.dropdown}
|
||||||
|
value={value}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={handleChange}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
3
components/input/WebsiteDateFilter.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
.dropdown {
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
|
|
||||||
.row > .col {
|
.row > .col {
|
||||||
border-top: 1px solid var(--base300);
|
border-top: 1px solid var(--base300);
|
||||||
|
border-inline-start: 0;
|
||||||
border-inline-end: 0;
|
border-inline-end: 0;
|
||||||
padding: 20px 0;
|
padding: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@ export function NavBar() {
|
||||||
|
|
||||||
const links = [
|
const links = [
|
||||||
{ label: formatMessage(labels.dashboard), url: '/dashboard' },
|
{ 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' },
|
!cloudMode && { label: formatMessage(labels.settings), url: '/settings' },
|
||||||
].filter(n => n);
|
].filter(n => n);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -158,6 +158,8 @@ export const labels = defineMessages({
|
||||||
max: { id: 'labels.max', defaultMessage: 'Max' },
|
max: { id: 'labels.max', defaultMessage: 'Max' },
|
||||||
unique: { id: 'labels.unique', defaultMessage: 'Unique' },
|
unique: { id: 'labels.unique', defaultMessage: 'Unique' },
|
||||||
value: { id: 'labels.value', defaultMessage: 'Value' },
|
value: { id: 'labels.value', defaultMessage: 'Value' },
|
||||||
|
overview: { id: 'labels.overview', defaultMessage: 'Overview' },
|
||||||
|
totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const messages = defineMessages({
|
export const messages = defineMessages({
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,23 @@ import FilterLink from 'components/common/FilterLink';
|
||||||
import MetricsTable from 'components/metrics/MetricsTable';
|
import MetricsTable from 'components/metrics/MetricsTable';
|
||||||
import { BROWSERS } from 'lib/constants';
|
import { BROWSERS } from 'lib/constants';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export function BrowsersTable({ websiteId, ...props }) {
|
export function BrowsersTable({ websiteId, ...props }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
function renderLink({ x: browser }) {
|
function renderLink({ x: browser }) {
|
||||||
return <FilterLink id="browser" value={browser} label={BROWSERS[browser] || browser} />;
|
return (
|
||||||
|
<FilterLink id="browser" value={browser} label={BROWSERS[browser] || browser}>
|
||||||
|
<img
|
||||||
|
src={`${basePath}/images/browsers/${browser || 'unknown'}.png`}
|
||||||
|
alt={browser}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
</FilterLink>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import MetricsTable from './MetricsTable';
|
import { useRouter } from 'next/router';
|
||||||
import FilterLink from 'components/common/FilterLink';
|
import FilterLink from 'components/common/FilterLink';
|
||||||
import useCountryNames from 'hooks/useCountryNames';
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
|
||||||
export function CountriesTable({ websiteId, ...props }) {
|
export function CountriesTable({ websiteId, ...props }) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const countryNames = useCountryNames(locale);
|
const countryNames = useCountryNames(locale);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
function renderLink({ x: code }) {
|
function renderLink({ x: code }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -17,7 +19,7 @@ export function CountriesTable({ websiteId, ...props }) {
|
||||||
value={countryNames[code] && code}
|
value={countryNames[code] && code}
|
||||||
label={countryNames[code]}
|
label={countryNames[code]}
|
||||||
>
|
>
|
||||||
<img src={`/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
|
<img src={`${basePath}/images/flags/${code?.toLowerCase() || 'xx'}.png`} alt={code} />
|
||||||
</FilterLink>
|
</FilterLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import FilterLink from 'components/common/FilterLink';
|
import FilterLink from 'components/common/FilterLink';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export function DevicesTable({ websiteId, ...props }) {
|
export function DevicesTable({ websiteId, ...props }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
function renderLink({ x: device }) {
|
function renderLink({ x: device }) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -11,7 +13,14 @@ export function DevicesTable({ websiteId, ...props }) {
|
||||||
id="device"
|
id="device"
|
||||||
value={labels[device] && device}
|
value={labels[device] && device}
|
||||||
label={formatMessage(labels[device] || labels.unknown)}
|
label={formatMessage(labels[device] || labels.unknown)}
|
||||||
/>
|
>
|
||||||
|
<img
|
||||||
|
src={`${basePath}/images/device/${device.toLowerCase() || 'unknown'}.png`}
|
||||||
|
alt={device}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
</FilterLink>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,114 +1,13 @@
|
||||||
import { useState } from 'react';
|
|
||||||
import { Loading } from 'react-basics';
|
import { Loading } from 'react-basics';
|
||||||
import ErrorMessage from 'components/common/ErrorMessage';
|
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';
|
import styles from './MetricsBar.module.css';
|
||||||
|
|
||||||
export function MetricsBar({ websiteId }) {
|
export function MetricsBar({ children, onClick, isLoading, isFetched, error }) {
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.bar} onClick={handleSetFormat}>
|
<div className={styles.bar} onClick={onClick}>
|
||||||
{isLoading && !isFetched && <Loading icon="dots" />}
|
{isLoading && !isFetched && <Loading icon="dots" />}
|
||||||
{error && <ErrorMessage />}
|
{error && <ErrorMessage />}
|
||||||
{data && !error && isFetched && (
|
{children}
|
||||||
<>
|
|
||||||
<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'], ' ')}`}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
.bar {
|
.bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
min-height: 110px;
|
min-height: 110px;
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,25 @@
|
||||||
import MetricsTable from './MetricsTable';
|
import MetricsTable from './MetricsTable';
|
||||||
import FilterLink from 'components/common/FilterLink';
|
import FilterLink from 'components/common/FilterLink';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export function OSTable({ websiteId, ...props }) {
|
export function OSTable({ websiteId, ...props }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
function renderLink({ x: os }) {
|
function renderLink({ x: os }) {
|
||||||
return <FilterLink id="os" value={os} />;
|
return (
|
||||||
|
<FilterLink id="os" value={os}>
|
||||||
|
<img
|
||||||
|
src={`${basePath}/images/os/${
|
||||||
|
os.toLowerCase().replaceAll(/[^\w]+/g, '-') || 'unknown'
|
||||||
|
}.png`}
|
||||||
|
alt={os}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
/>
|
||||||
|
</FilterLink>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
import MetricsTable from './MetricsTable';
|
import { useRouter } from 'next/router';
|
||||||
import { emptyFilter } from 'lib/filters';
|
|
||||||
import FilterLink from 'components/common/FilterLink';
|
import FilterLink from 'components/common/FilterLink';
|
||||||
|
import { emptyFilter } from 'lib/filters';
|
||||||
import useLocale from 'hooks/useLocale';
|
import useLocale from 'hooks/useLocale';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
import useCountryNames from 'hooks/useCountryNames';
|
import useCountryNames from 'hooks/useCountryNames';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
import regions from 'public/iso-3166-2.json';
|
import regions from 'public/iso-3166-2.json';
|
||||||
|
|
||||||
export function RegionsTable({ websiteId, ...props }) {
|
export function RegionsTable({ websiteId, ...props }) {
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const countryNames = useCountryNames(locale);
|
const countryNames = useCountryNames(locale);
|
||||||
|
const { basePath } = useRouter();
|
||||||
|
|
||||||
const renderLabel = x => {
|
const renderLabel = x => {
|
||||||
return regions[x] ? `${regions[x]}, ${countryNames[x.split('-')[0]]}` : x;
|
return regions[x] ? `${regions[x]}, ${countryNames[x.split('-')[0]]}` : x;
|
||||||
|
|
@ -18,7 +20,10 @@ export function RegionsTable({ websiteId, ...props }) {
|
||||||
const renderLink = ({ x: code }) => {
|
const renderLink = ({ x: code }) => {
|
||||||
return (
|
return (
|
||||||
<FilterLink id="region" className={locale} value={code} label={renderLabel(code)}>
|
<FilterLink id="region" className={locale} value={code} label={renderLabel(code)}>
|
||||||
<img src={`/images/flags/${code?.split('-')?.[0]?.toLowerCase() || 'xx'}.png`} alt={code} />
|
<img
|
||||||
|
src={`${basePath}/images/flags/${code?.split('-')?.[0]?.toLowerCase() || 'xx'}.png`}
|
||||||
|
alt={code}
|
||||||
|
/>
|
||||||
</FilterLink>
|
</FilterLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { Row, Column, Text } from 'react-basics';
|
|
||||||
import Favicon from 'components/common/Favicon';
|
|
||||||
import ActiveUsers from './ActiveUsers';
|
|
||||||
import styles from './WebsiteHeader.module.css';
|
|
||||||
|
|
||||||
export function WebsiteHeader({ websiteId, name, domain, children }) {
|
|
||||||
return (
|
|
||||||
<Row className={styles.header} justifyContent="center">
|
|
||||||
<Column className={styles.title} variant="two">
|
|
||||||
<Favicon domain={domain} />
|
|
||||||
<Text>{name}</Text>
|
|
||||||
</Column>
|
|
||||||
<Column className={styles.info} variant="two">
|
|
||||||
<ActiveUsers websiteId={websiteId} />
|
|
||||||
{children}
|
|
||||||
</Column>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WebsiteHeader;
|
|
||||||
|
|
@ -2,7 +2,7 @@ import WebsiteSelect from 'components/input/WebsiteSelect';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import EventsChart from 'components/metrics/EventsChart';
|
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 useApi from 'hooks/useApi';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
@ -143,12 +143,7 @@ export function TestConsole() {
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
<Column>
|
<Column>
|
||||||
<WebsiteChart
|
<WebsiteChart websiteId={website.id} />
|
||||||
websiteId={website.id}
|
|
||||||
name={website.name}
|
|
||||||
domain={website.domain}
|
|
||||||
showLink
|
|
||||||
/>
|
|
||||||
<EventsChart websiteId={website.id} />
|
<EventsChart websiteId={website.id} />
|
||||||
</Column>
|
</Column>
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
47
components/pages/event-data/EventDataMetricsBar.js
Normal 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 className={styles.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;
|
||||||
46
components/pages/event-data/EventDataMetricsBar.module.css
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
32
components/pages/event-data/EventDataTable.js
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
|
import { useMessages, usePageQuery } from 'hooks';
|
||||||
|
import Empty from 'components/common/Empty';
|
||||||
|
|
||||||
|
export function EventDataTable({ data = [], showValue }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { resolveUrl } = usePageQuery();
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return <Empty />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridTable data={data}>
|
||||||
|
<GridColumn name="field" label={formatMessage(labels.field)}>
|
||||||
|
{row => {
|
||||||
|
return (
|
||||||
|
<Link href={resolveUrl({ view: row.field })} shallow={true}>
|
||||||
|
{row.field}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</GridColumn>
|
||||||
|
<GridColumn name="total" label={formatMessage(labels.totalRecords)}>
|
||||||
|
{({ total }) => total.toLocaleString()}
|
||||||
|
</GridColumn>
|
||||||
|
</GridTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventDataTable;
|
||||||
44
components/pages/event-data/EventDataValueTable.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { GridTable, GridColumn, Button, Icon, Text, Flexbox } from 'react-basics';
|
||||||
|
import { useMessages, usePageQuery } from 'hooks';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
import Empty from 'components/common/Empty';
|
||||||
|
|
||||||
|
export function EventDataTable({ data = [], field }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { resolveUrl } = usePageQuery();
|
||||||
|
|
||||||
|
const Title = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Link href={resolveUrl({ view: undefined })}>
|
||||||
|
<Button>
|
||||||
|
<Icon rotate={180}>
|
||||||
|
<Icons.ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.back)}</Text>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
<Text>{field}</Text>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHeader title={<Title />} />
|
||||||
|
{data.length <= 0 && <Empty />}
|
||||||
|
{data.length > 0 && (
|
||||||
|
<GridTable data={data}>
|
||||||
|
<GridColumn name="value" label={formatMessage(labels.value)} />
|
||||||
|
<GridColumn name="total" label={formatMessage(labels.totalRecords)} width="200px">
|
||||||
|
{({ total }) => total.toLocaleString()}
|
||||||
|
</GridColumn>
|
||||||
|
</GridTable>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventDataTable;
|
||||||
|
|
@ -1,22 +1,20 @@
|
||||||
import { useState, useEffect, useMemo } from 'react';
|
import { useState, useEffect, useMemo } from 'react';
|
||||||
import { subMinutes, startOfMinute } from 'date-fns';
|
import { subMinutes, startOfMinute } from 'date-fns';
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import firstBy from 'thenby';
|
import firstBy from 'thenby';
|
||||||
import { GridRow, GridColumn } from 'components/layout/Grid';
|
import { GridRow, GridColumn } from 'components/layout/Grid';
|
||||||
import Page from 'components/layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import RealtimeChart from 'components/metrics/RealtimeChart';
|
import RealtimeChart from 'components/metrics/RealtimeChart';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
|
||||||
import WorldMap from 'components/common/WorldMap';
|
import WorldMap from 'components/common/WorldMap';
|
||||||
import RealtimeLog from 'components/pages/realtime/RealtimeLog';
|
import RealtimeLog from 'components/pages/realtime/RealtimeLog';
|
||||||
import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
|
import RealtimeHeader from 'components/pages/realtime/RealtimeHeader';
|
||||||
import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
|
import RealtimeUrls from 'components/pages/realtime/RealtimeUrls';
|
||||||
import RealtimeCountries from 'components/pages/realtime/RealtimeCountries';
|
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 useApi from 'hooks/useApi';
|
||||||
import useMessages from 'hooks/useMessages';
|
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants';
|
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) {
|
function mergeData(state = [], data = [], time) {
|
||||||
const ids = state.map(({ __id }) => __id);
|
const ids = state.map(({ __id }) => __id);
|
||||||
|
|
@ -25,12 +23,10 @@ function mergeData(state = [], data = [], time) {
|
||||||
.filter(({ timestamp }) => timestamp >= time);
|
.filter(({ timestamp }) => timestamp >= time);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RealtimeDashboard({ websiteId }) {
|
export function RealtimePage({ websiteId }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const router = useRouter();
|
|
||||||
const [currentData, setCurrentData] = useState();
|
const [currentData, setCurrentData] = useState();
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`));
|
const { data: website } = useWebsite(websiteId);
|
||||||
const { data, isLoading, error } = useQuery(
|
const { data, isLoading, error } = useQuery(
|
||||||
['realtime', websiteId],
|
['realtime', websiteId],
|
||||||
() => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
|
() => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }),
|
||||||
|
|
@ -93,15 +89,9 @@ export function RealtimeDashboard({ websiteId }) {
|
||||||
return currentData;
|
return currentData;
|
||||||
}, [currentData]);
|
}, [currentData]);
|
||||||
|
|
||||||
const handleSelect = id => {
|
|
||||||
router.push(`/realtime/${id}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<Page loading={isLoading} error={error}>
|
||||||
<PageHeader title={formatMessage(labels.realtime)}>
|
<WebsiteHeader websiteId={websiteId} />
|
||||||
<WebsiteSelect websiteId={websiteId} onSelect={handleSelect} />
|
|
||||||
</PageHeader>
|
|
||||||
<RealtimeHeader websiteId={websiteId} data={currentData} />
|
<RealtimeHeader websiteId={websiteId} data={currentData} />
|
||||||
<div className={styles.chart}>
|
<div className={styles.chart}>
|
||||||
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
|
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
|
||||||
|
|
@ -126,4 +116,4 @@ export function RealtimeDashboard({ websiteId }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default RealtimeDashboard;
|
export default RealtimePage;
|
||||||
|
|
@ -9,24 +9,12 @@ import styles from './ReportTemplates.module.css';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
|
|
||||||
const reports = [
|
const reports = [
|
||||||
{
|
|
||||||
title: 'Event data',
|
|
||||||
description: 'Query your custom event data.',
|
|
||||||
url: '/reports/event-data',
|
|
||||||
icon: <Nodes />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
title: 'Funnel',
|
title: 'Funnel',
|
||||||
description: 'Understand the conversion and drop-off rate of users.',
|
description: 'Understand the conversion and drop-off rate of users.',
|
||||||
url: '/reports/funnel',
|
url: '/reports/funnel',
|
||||||
icon: <Funnel />,
|
icon: <Funnel />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Insights',
|
|
||||||
description: 'Explore your data by applying segments and filters.',
|
|
||||||
url: '/reports/insights',
|
|
||||||
icon: <Lightbulb />,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function ReportItem({ title, description, url, icon }) {
|
function ReportItem({ title, description, url, icon }) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
|
width: 360px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.report {
|
.report {
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,13 @@ import { Button, Icon, Icons, Text } from 'react-basics';
|
||||||
import { useMessages, useReports } from 'hooks';
|
import { useMessages, useReports } from 'hooks';
|
||||||
import ReportsTable from './ReportsTable';
|
import ReportsTable from './ReportsTable';
|
||||||
|
|
||||||
export function ReportsList() {
|
export function ReportsPage() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { reports, error, isLoading } = useReports();
|
const { reports, error, isLoading } = useReports();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page loading={isLoading} error={error}>
|
<Page loading={isLoading} error={error}>
|
||||||
<PageHeader title="Reports">
|
<PageHeader title={formatMessage(labels.reports)}>
|
||||||
<Link href="/reports/create">
|
<Link href="/reports/create">
|
||||||
<Button variant="primary">
|
<Button variant="primary">
|
||||||
<Icon>
|
<Icon>
|
||||||
|
|
@ -26,4 +26,4 @@ export function ReportsList() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReportsList;
|
export default ReportsPage;
|
||||||
|
|
@ -41,7 +41,6 @@ export function EventDataParameters() {
|
||||||
const parameterGroups = [
|
const parameterGroups = [
|
||||||
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
||||||
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
|
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
|
||||||
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const parameterData = {
|
const parameterData = {
|
||||||
|
|
@ -55,11 +54,9 @@ export function EventDataParameters() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleAdd = (group, value) => {
|
const handleAdd = (group, value) => {
|
||||||
const data = parameterData[group];
|
const data = parameterData[group].filter(({ name }) => name !== value.name);
|
||||||
|
|
||||||
if (!data.find(({ name }) => name === value.name)) {
|
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||||
updateReport({ parameters: { [group]: data.concat(value) } });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = (group, index) => {
|
const handleRemove = (group, index) => {
|
||||||
|
|
@ -127,11 +124,6 @@ export function EventDataParameters() {
|
||||||
<div>{value[1]}</div>
|
<div>{value[1]}</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{group === REPORT_PARAMETERS.groups && (
|
|
||||||
<>
|
|
||||||
<div>{name}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ import ReportHeader from '../ReportHeader';
|
||||||
import ReportMenu from '../ReportMenu';
|
import ReportMenu from '../ReportMenu';
|
||||||
import ReportBody from '../ReportBody';
|
import ReportBody from '../ReportBody';
|
||||||
import EventDataParameters from './EventDataParameters';
|
import EventDataParameters from './EventDataParameters';
|
||||||
import Nodes from 'assets/nodes.svg';
|
|
||||||
import EventDataTable from './EventDataTable';
|
import EventDataTable from './EventDataTable';
|
||||||
|
import Nodes from 'assets/nodes.svg';
|
||||||
|
|
||||||
const defaultParameters = {
|
const defaultParameters = {
|
||||||
type: 'event-data',
|
type: 'event-data',
|
||||||
parameters: { fields: [], filters: [], groups: [] },
|
parameters: { fields: [], filters: [] },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function EventDataReport({ reportId }) {
|
export default function EventDataReport({ reportId }) {
|
||||||
|
|
|
||||||
44
components/pages/reports/insights/FieldAddForm.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { createPortal } from 'react-dom';
|
||||||
|
import { REPORT_PARAMETERS } from 'lib/constants';
|
||||||
|
import PopupForm from '../PopupForm';
|
||||||
|
import FieldSelectForm from '../FieldSelectForm';
|
||||||
|
import FieldAggregateForm from '../FieldAggregateForm';
|
||||||
|
import FieldFilterForm from '../FieldFilterForm';
|
||||||
|
import styles from './FieldAddForm.module.css';
|
||||||
|
|
||||||
|
export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) {
|
||||||
|
const [selected, setSelected] = useState();
|
||||||
|
|
||||||
|
const handleSelect = value => {
|
||||||
|
const { type } = value;
|
||||||
|
|
||||||
|
if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') {
|
||||||
|
value.value = group === REPORT_PARAMETERS.groups ? '' : 'total';
|
||||||
|
handleSave(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelected(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = value => {
|
||||||
|
onAdd(group, value);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return createPortal(
|
||||||
|
<PopupForm className={styles.popup} element={element} onClose={onClose}>
|
||||||
|
{!selected && <FieldSelectForm fields={fields} onSelect={handleSelect} />}
|
||||||
|
{selected && group === REPORT_PARAMETERS.fields && (
|
||||||
|
<FieldAggregateForm {...selected} onSelect={handleSave} />
|
||||||
|
)}
|
||||||
|
{selected && group === REPORT_PARAMETERS.filters && (
|
||||||
|
<FieldFilterForm {...selected} onSelect={handleSave} />
|
||||||
|
)}
|
||||||
|
</PopupForm>,
|
||||||
|
document.body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FieldAddForm;
|
||||||
38
components/pages/reports/insights/FieldAddForm.module.css
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
.menu {
|
||||||
|
width: 360px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item:hover {
|
||||||
|
background: var(--base75);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type {
|
||||||
|
color: var(--font-color300);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
min-width: 60px;
|
||||||
|
}
|
||||||
151
components/pages/reports/insights/InsightsParameters.js
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import { useContext, useRef } from 'react';
|
||||||
|
import { useApi, useMessages } from 'hooks';
|
||||||
|
import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics';
|
||||||
|
import { ReportContext } from 'components/pages/reports/Report';
|
||||||
|
import Empty from 'components/common/Empty';
|
||||||
|
import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants';
|
||||||
|
import Icons from 'components/icons';
|
||||||
|
import FieldAddForm from './FieldAddForm';
|
||||||
|
import BaseParameters from '../BaseParameters';
|
||||||
|
import ParameterList from '../ParameterList';
|
||||||
|
import styles from './InsightsParameters.module.css';
|
||||||
|
|
||||||
|
function useFields(websiteId, startDate, endDate) {
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { data, error, isLoading } = useQuery(
|
||||||
|
['fields', websiteId, startDate, endDate],
|
||||||
|
() =>
|
||||||
|
get('/reports/event-data', {
|
||||||
|
websiteId,
|
||||||
|
startAt: +startDate,
|
||||||
|
endAt: +endDate,
|
||||||
|
}),
|
||||||
|
{ enabled: !!(websiteId && startDate && endDate) },
|
||||||
|
);
|
||||||
|
|
||||||
|
return { data, error, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InsightsParameters() {
|
||||||
|
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
|
||||||
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
|
const ref = useRef(null);
|
||||||
|
const { parameters } = report || {};
|
||||||
|
const { websiteId, dateRange, fields, filters, groups } = parameters || {};
|
||||||
|
const { startDate, endDate } = dateRange || {};
|
||||||
|
const queryEnabled = websiteId && dateRange && fields?.length;
|
||||||
|
const { data, error } = useFields(websiteId, startDate, endDate);
|
||||||
|
const parametersSelected = websiteId && startDate && endDate;
|
||||||
|
const hasData = data?.length !== 0;
|
||||||
|
|
||||||
|
const parameterGroups = [
|
||||||
|
{ label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields },
|
||||||
|
{ label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters },
|
||||||
|
{ label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups },
|
||||||
|
];
|
||||||
|
|
||||||
|
const parameterData = {
|
||||||
|
fields,
|
||||||
|
filters,
|
||||||
|
groups,
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = values => {
|
||||||
|
runReport(values);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAdd = (group, value) => {
|
||||||
|
const data = parameterData[group];
|
||||||
|
|
||||||
|
if (!data.find(({ name }) => name === value.name)) {
|
||||||
|
updateReport({ parameters: { [group]: data.concat(value) } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemove = (group, index) => {
|
||||||
|
const data = [...parameterData[group]];
|
||||||
|
data.splice(index, 1);
|
||||||
|
updateReport({ parameters: { [group]: data } });
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddButton = ({ group }) => {
|
||||||
|
return (
|
||||||
|
<PopupTrigger>
|
||||||
|
<Icon>
|
||||||
|
<Icons.Plus />
|
||||||
|
</Icon>
|
||||||
|
<Popup position="bottom" alignment="start">
|
||||||
|
{(close, element) => {
|
||||||
|
return (
|
||||||
|
<FieldAddForm
|
||||||
|
fields={data.map(({ eventKey, InsightsType }) => ({
|
||||||
|
name: eventKey,
|
||||||
|
type: DATA_TYPES[InsightsType],
|
||||||
|
}))}
|
||||||
|
group={group}
|
||||||
|
element={element}
|
||||||
|
onAdd={handleAdd}
|
||||||
|
onClose={close}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Popup>
|
||||||
|
</PopupTrigger>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form ref={ref} values={parameters} error={error} onSubmit={handleSubmit}>
|
||||||
|
<BaseParameters />
|
||||||
|
{!hasData && <Empty message={formatMessage(messages.noInsights)} />}
|
||||||
|
{parametersSelected &&
|
||||||
|
hasData &&
|
||||||
|
parameterGroups.map(({ label, group }) => {
|
||||||
|
return (
|
||||||
|
<FormRow
|
||||||
|
key={label}
|
||||||
|
label={label}
|
||||||
|
action={<AddButton group={group} onAdd={handleAdd} />}
|
||||||
|
>
|
||||||
|
<ParameterList
|
||||||
|
items={parameterData[group]}
|
||||||
|
onRemove={index => handleRemove(group, index)}
|
||||||
|
>
|
||||||
|
{({ name, value }) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.parameter}>
|
||||||
|
{group === REPORT_PARAMETERS.fields && (
|
||||||
|
<>
|
||||||
|
<div>{name}</div>
|
||||||
|
<div className={styles.op}>{value}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{group === REPORT_PARAMETERS.filters && (
|
||||||
|
<>
|
||||||
|
<div>{name}</div>
|
||||||
|
<div className={styles.op}>{value[0]}</div>
|
||||||
|
<div>{value[1]}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{group === REPORT_PARAMETERS.groups && (
|
||||||
|
<>
|
||||||
|
<div>{name}</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</ParameterList>
|
||||||
|
</FormRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<FormButtons>
|
||||||
|
<SubmitButton variant="primary" disabled={!queryEnabled} loading={isRunning}>
|
||||||
|
{formatMessage(labels.runQuery)}
|
||||||
|
</SubmitButton>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InsightsParameters;
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
.parameter {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
26
components/pages/reports/insights/InsightsReport.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import Report from '../Report';
|
||||||
|
import ReportHeader from '../ReportHeader';
|
||||||
|
import ReportMenu from '../ReportMenu';
|
||||||
|
import ReportBody from '../ReportBody';
|
||||||
|
import InsightsParameters from './InsightsParameters';
|
||||||
|
import InsightsTable from './InsightsTable';
|
||||||
|
import Lightbulb from 'assets/lightbulb.svg';
|
||||||
|
|
||||||
|
const defaultParameters = {
|
||||||
|
type: 'insights',
|
||||||
|
parameters: { fields: [], filters: [], groups: [] },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function InsightsReport({ reportId }) {
|
||||||
|
return (
|
||||||
|
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||||
|
<ReportHeader icon={<Lightbulb />} />
|
||||||
|
<ReportMenu>
|
||||||
|
<InsightsParameters />
|
||||||
|
</ReportMenu>
|
||||||
|
<ReportBody>
|
||||||
|
<InsightsTable />
|
||||||
|
</ReportBody>
|
||||||
|
</Report>
|
||||||
|
);
|
||||||
|
}
|
||||||
19
components/pages/reports/insights/InsightsTable.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { GridTable, GridColumn } from 'react-basics';
|
||||||
|
import { useMessages } from 'hooks';
|
||||||
|
import { ReportContext } from '../Report';
|
||||||
|
|
||||||
|
export function InsightsTable() {
|
||||||
|
const { report } = useContext(ReportContext);
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GridTable data={report?.data || []}>
|
||||||
|
<GridColumn name="field" label={formatMessage(labels.field)} />
|
||||||
|
<GridColumn name="value" label={formatMessage(labels.value)} />
|
||||||
|
<GridColumn name="total" label={formatMessage(labels.total)} />
|
||||||
|
</GridTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default InsightsTable;
|
||||||
59
components/pages/websites/WebsiteChart.js
Normal 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;
|
||||||
17
components/pages/websites/WebsiteChart.module.css
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,19 @@
|
||||||
|
import { Button, Text, Icon } from 'react-basics';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { firstBy } from 'thenby';
|
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 useDashboard from 'store/dashboard';
|
||||||
import styles from './WebsiteList.module.css';
|
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 }) {
|
export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteOrder } = useDashboard();
|
const { websiteOrder } = useDashboard();
|
||||||
|
const { dir } = useLocale();
|
||||||
|
|
||||||
const ordered = useMemo(
|
const ordered = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -17,16 +25,23 @@ export default function WebsiteChartList({ websites, showCharts, limit }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ordered.map(({ id, name, domain }, index) => {
|
{ordered.map(({ id }, index) => {
|
||||||
return index < limit ? (
|
return index < limit ? (
|
||||||
<div key={id} className={styles.website}>
|
<div key={id} className={styles.website}>
|
||||||
<WebsiteChart
|
<WebsiteHeader websiteId={id} showLinks={false}>
|
||||||
websiteId={id}
|
<Link href={`/websites/${id}`}>
|
||||||
name={name}
|
<Button variant="primary">
|
||||||
domain={domain}
|
<Text>{formatMessage(labels.viewDetails)}</Text>
|
||||||
showChart={showCharts}
|
<Icon>
|
||||||
showDetailsButton={true}
|
<Icon rotate={dir === 'rtl' ? 180 : 0}>
|
||||||
/>
|
<Icons.ArrowRight />
|
||||||
|
</Icon>
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</WebsiteHeader>
|
||||||
|
<WebsiteMetricsBar websiteId={id} />
|
||||||
|
<WebsiteChart websiteId={id} showChart={showCharts} />
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
37
components/pages/websites/WebsiteDetailsPage.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
39
components/pages/websites/WebsiteEventData.js
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { Flexbox } from 'react-basics';
|
||||||
|
import EventDataTable from 'components/pages/event-data/EventDataTable';
|
||||||
|
import EventDataValueTable from 'components/pages/event-data/EventDataValueTable';
|
||||||
|
import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar';
|
||||||
|
import { useDateRange, useApi, usePageQuery } from 'hooks';
|
||||||
|
|
||||||
|
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, field }],
|
||||||
|
() =>
|
||||||
|
get('/event-data', {
|
||||||
|
websiteId,
|
||||||
|
startAt: +startDate,
|
||||||
|
endAt: +endDate,
|
||||||
|
field,
|
||||||
|
}),
|
||||||
|
{ enabled: !!(websiteId && startDate && endDate) },
|
||||||
|
);
|
||||||
|
|
||||||
|
return { data, error, isLoading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function WebsiteEventData({ websiteId }) {
|
||||||
|
const {
|
||||||
|
query: { view },
|
||||||
|
} = usePageQuery();
|
||||||
|
const { data } = useFields(websiteId, view);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flexbox direction="column" gap={20}>
|
||||||
|
<EventDataMetricsBar websiteId={websiteId} />
|
||||||
|
{!view && <EventDataTable data={data} />}
|
||||||
|
{view && <EventDataValueTable field={view} data={data} />}
|
||||||
|
</Flexbox>
|
||||||
|
);
|
||||||
|
}
|
||||||
4
components/pages/websites/WebsiteEventData.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
12
components/pages/websites/WebsiteEventDataPage.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
components/pages/websites/WebsiteHeader.js
Normal 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;
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
height: 100px;
|
height: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -23,3 +23,7 @@
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.selected {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
138
components/pages/websites/WebsiteMetricsBar.js
Normal 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;
|
||||||
|
|
@ -1,22 +1,4 @@
|
||||||
.container {
|
.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;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
@ -35,8 +17,10 @@
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown {
|
@media only screen and (max-width: 1200px) {
|
||||||
min-width: 200px;
|
.actions {
|
||||||
|
margin-top: 40px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (min-width: 992px) {
|
@media only screen and (min-width: 992px) {
|
||||||
|
|
@ -49,9 +33,3 @@
|
||||||
border-bottom: 1px solid var(--base300);
|
border-bottom: 1px solid var(--base300);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
|
||||||
.actions {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
30
components/pages/websites/WebsiteReportsPage.js
Normal 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;
|
||||||
|
|
@ -18,3 +18,4 @@ export * from './useSticky';
|
||||||
export * from './useTheme';
|
export * from './useTheme';
|
||||||
export * from './useTimezone';
|
export * from './useTimezone';
|
||||||
export * from './useUser';
|
export * from './useUser';
|
||||||
|
export * from './useWebsite';
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import useApi from './useApi';
|
import useApi from './useApi';
|
||||||
|
|
||||||
export function useReports() {
|
export function useReports(websiteId) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`));
|
const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`, { websiteId }));
|
||||||
|
|
||||||
return { reports: data, error, isLoading };
|
return { reports: data, error, isLoading };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
hooks/useWebsite.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import useApi from './useApi';
|
||||||
|
|
||||||
|
export function useWebsite(websiteId) {
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
return useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), {
|
||||||
|
enabled: !!websiteId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useWebsite;
|
||||||
|
|
@ -167,7 +167,8 @@ export const EVENT_COLORS = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DOMAIN_REGEX =
|
export const DOMAIN_REGEX =
|
||||||
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/;
|
/^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9-]+(-[a-z0-9-]+)*\.)+(xn--)?[a-z0-9-]{2,63})$/;
|
||||||
|
|
||||||
|
|
||||||
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{16}$/;
|
export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{16}$/;
|
||||||
|
|
||||||
|
|
@ -182,7 +183,7 @@ export const DESKTOP_OS = [
|
||||||
'BeOS',
|
'BeOS',
|
||||||
'Chrome OS',
|
'Chrome OS',
|
||||||
'Linux',
|
'Linux',
|
||||||
'macOS',
|
'Mac OS',
|
||||||
'Open BSD',
|
'Open BSD',
|
||||||
'OS/2',
|
'OS/2',
|
||||||
'QNX',
|
'QNX',
|
||||||
|
|
@ -204,33 +205,34 @@ export const DESKTOP_OS = [
|
||||||
export const MOBILE_OS = ['Amazon OS', 'Android OS', 'BlackBerry OS', 'iOS', 'Windows Mobile'];
|
export const MOBILE_OS = ['Amazon OS', 'Android OS', 'BlackBerry OS', 'iOS', 'Windows Mobile'];
|
||||||
|
|
||||||
export const BROWSERS = {
|
export const BROWSERS = {
|
||||||
|
android: 'Android',
|
||||||
aol: 'AOL',
|
aol: 'AOL',
|
||||||
edge: 'Edge',
|
|
||||||
'edge-ios': 'Edge (iOS)',
|
|
||||||
yandexbrowser: 'Yandex',
|
|
||||||
kakaotalk: 'KaKaoTalk',
|
|
||||||
samsung: 'Samsung',
|
|
||||||
silk: 'Silk',
|
|
||||||
miui: 'MIUI',
|
|
||||||
beaker: 'Beaker',
|
beaker: 'Beaker',
|
||||||
'edge-chromium': 'Edge (Chromium)',
|
bb10: 'BlackBerry 10',
|
||||||
chrome: 'Chrome',
|
chrome: 'Chrome',
|
||||||
'chromium-webview': 'Chrome (webview)',
|
'chromium-webview': 'Chrome (webview)',
|
||||||
phantomjs: 'PhantomJS',
|
|
||||||
crios: 'Chrome (iOS)',
|
crios: 'Chrome (iOS)',
|
||||||
|
curl: 'Curl',
|
||||||
|
edge: 'Edge',
|
||||||
|
'edge-chromium': 'Edge (Chromium)',
|
||||||
|
'edge-ios': 'Edge (iOS)',
|
||||||
|
facebook: 'Facebook',
|
||||||
firefox: 'Firefox',
|
firefox: 'Firefox',
|
||||||
fxios: 'Firefox (iOS)',
|
fxios: 'Firefox (iOS)',
|
||||||
'opera-mini': 'Opera Mini',
|
|
||||||
opera: 'Opera',
|
|
||||||
ie: 'IE',
|
ie: 'IE',
|
||||||
bb10: 'BlackBerry 10',
|
|
||||||
android: 'Android',
|
|
||||||
ios: 'iOS',
|
|
||||||
safari: 'Safari',
|
|
||||||
facebook: 'Facebook',
|
|
||||||
instagram: 'Instagram',
|
instagram: 'Instagram',
|
||||||
|
ios: 'iOS',
|
||||||
'ios-webview': 'iOS (webview)',
|
'ios-webview': 'iOS (webview)',
|
||||||
|
kakaotalk: 'KaKaoTalk',
|
||||||
|
miui: 'MIUI',
|
||||||
|
opera: 'Opera',
|
||||||
|
'opera-mini': 'Opera Mini',
|
||||||
|
phantomjs: 'PhantomJS',
|
||||||
|
safari: 'Safari',
|
||||||
|
samsung: 'Samsung',
|
||||||
|
silk: 'Silk',
|
||||||
searchbot: 'Searchbot',
|
searchbot: 'Searchbot',
|
||||||
|
yandexbrowser: 'Yandex',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MAP_FILE = '/datamaps.world.json';
|
export const MAP_FILE = '/datamaps.world.json';
|
||||||
|
|
|
||||||
|
|
@ -95,7 +95,7 @@
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-basics": "^0.89.0",
|
"react-basics": "^0.91.0",
|
||||||
"react-beautiful-dnd": "^13.1.0",
|
"react-beautiful-dnd": "^13.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-error-boundary": "^4.0.4",
|
"react-error-boundary": "^4.0.4",
|
||||||
|
|
@ -105,7 +105,7 @@
|
||||||
"react-use-measure": "^2.0.4",
|
"react-use-measure": "^2.0.4",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"request-ip": "^3.3.0",
|
"request-ip": "^3.3.0",
|
||||||
"semver": "^7.3.6",
|
"semver": "^7.5.2",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"timezone-support": "^2.0.2",
|
"timezone-support": "^2.0.2",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
|
|
@ -151,7 +151,7 @@
|
||||||
"rollup-plugin-node-externals": "^5.1.2",
|
"rollup-plugin-node-externals": "^5.1.2",
|
||||||
"rollup-plugin-postcss": "^4.0.2",
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"stylelint": "^14.16.1",
|
"stylelint": "^15.10.1",
|
||||||
"stylelint-config-css-modules": "^4.1.0",
|
"stylelint-config-css-modules": "^4.1.0",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^9.0.0",
|
"stylelint-config-recommended": "^9.0.0",
|
||||||
|
|
|
||||||
36
pages/api/event-data/fields.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { canViewWebsite } from 'lib/auth';
|
||||||
|
import { useCors, useAuth } from 'lib/middleware';
|
||||||
|
import { NextApiRequestQueryBody } from 'lib/types';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
|
import { getEventDataFields } from 'queries';
|
||||||
|
|
||||||
|
export interface EventDataFieldsRequestBody {
|
||||||
|
websiteId: string;
|
||||||
|
dateRange: {
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
req: NextApiRequestQueryBody<any, EventDataFieldsRequestBody>,
|
||||||
|
res: NextApiResponse<any>,
|
||||||
|
) => {
|
||||||
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const { websiteId, startAt, endAt } = req.query;
|
||||||
|
|
||||||
|
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt));
|
||||||
|
|
||||||
|
return ok(res, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
||||||
37
pages/api/event-data/index.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { canViewWebsite } from 'lib/auth';
|
||||||
|
import { useCors, useAuth } from 'lib/middleware';
|
||||||
|
import { NextApiRequestQueryBody } from 'lib/types';
|
||||||
|
import { NextApiResponse } from 'next';
|
||||||
|
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
|
import { getEventData } from 'queries';
|
||||||
|
|
||||||
|
export interface EventDataRequestBody {
|
||||||
|
websiteId: string;
|
||||||
|
dateRange: {
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
};
|
||||||
|
field?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async (
|
||||||
|
req: NextApiRequestQueryBody<any, EventDataRequestBody>,
|
||||||
|
res: NextApiResponse<any>,
|
||||||
|
) => {
|
||||||
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const { websiteId, startAt, endAt, field } = req.query;
|
||||||
|
|
||||||
|
if (!(await canViewWebsite(req.auth, websiteId))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getEventData(websiteId, new Date(+startAt), new Date(+endAt), field);
|
||||||
|
|
||||||
|
return ok(res, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
import { canViewWebsite } from 'lib/auth';
|
|
||||||
import { useCors, useAuth } from 'lib/middleware';
|
|
||||||
import { NextApiRequestQueryBody } from 'lib/types';
|
|
||||||
import { NextApiResponse } from 'next';
|
|
||||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
|
||||||
import { getEventDataFields } from 'queries/analytics/eventData/getEventDataFields';
|
|
||||||
import { getEventData } from 'queries';
|
|
||||||
|
|
||||||
export interface EventDataRequestBody {
|
|
||||||
websiteId: string;
|
|
||||||
dateRange: {
|
|
||||||
startDate: string;
|
|
||||||
endDate: string;
|
|
||||||
};
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
value: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
value: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
groups: [
|
|
||||||
{
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (
|
|
||||||
req: NextApiRequestQueryBody<any, EventDataRequestBody>,
|
|
||||||
res: NextApiResponse<any>,
|
|
||||||
) => {
|
|
||||||
await useCors(req, res);
|
|
||||||
await useAuth(req, res);
|
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
const { websiteId, startAt, endAt } = req.query;
|
|
||||||
|
|
||||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt));
|
|
||||||
|
|
||||||
return ok(res, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const {
|
|
||||||
websiteId,
|
|
||||||
dateRange: { startDate, endDate },
|
|
||||||
...criteria
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await getEventData(
|
|
||||||
websiteId,
|
|
||||||
new Date(startDate),
|
|
||||||
new Date(endDate),
|
|
||||||
criteria as any,
|
|
||||||
);
|
|
||||||
|
|
||||||
return ok(res, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
|
||||||
};
|
|
||||||
|
|
@ -2,8 +2,9 @@ import { uuid } from 'lib/crypto';
|
||||||
import { useAuth, useCors } from 'lib/middleware';
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
import { NextApiRequestQueryBody } from 'lib/types';
|
import { NextApiRequestQueryBody } from 'lib/types';
|
||||||
import { NextApiResponse } from 'next';
|
import { NextApiResponse } from 'next';
|
||||||
import { methodNotAllowed, ok } from 'next-basics';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { createReport, getReports } from 'queries';
|
import { createReport, getReports } from 'queries';
|
||||||
|
import { canViewWebsite } from 'lib/auth';
|
||||||
|
|
||||||
export interface ReportRequestBody {
|
export interface ReportRequestBody {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
|
@ -23,12 +24,18 @@ export default async (
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
const { websiteId } = req.query;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
user: { id: userId },
|
user: { id: userId },
|
||||||
} = req.auth;
|
} = req.auth;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const data = await getReports(userId);
|
if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getReports({ websiteId });
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
import { canViewWebsite } from 'lib/auth';
|
|
||||||
import { useAuth, useCors } from 'lib/middleware';
|
|
||||||
import { NextApiRequestQueryBody, WebsiteEventDataMetric } from 'lib/types';
|
|
||||||
import { NextApiResponse } from 'next';
|
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
|
||||||
import { getEventData } from 'queries';
|
|
||||||
|
|
||||||
export interface WebsiteEventDataRequestQuery {
|
|
||||||
id: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WebsiteEventDataRequestBody {
|
|
||||||
startAt: string;
|
|
||||||
endAt: string;
|
|
||||||
eventName?: string;
|
|
||||||
urlPath?: string;
|
|
||||||
timeSeries?: {
|
|
||||||
unit: string;
|
|
||||||
timezone: string;
|
|
||||||
};
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
eventKey?: string;
|
|
||||||
eventValue?: string | number | boolean | Date;
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async (
|
|
||||||
req: NextApiRequestQueryBody<WebsiteEventDataRequestQuery, WebsiteEventDataRequestBody>,
|
|
||||||
res: NextApiResponse<WebsiteEventDataMetric[]>,
|
|
||||||
) => {
|
|
||||||
await useCors(req, res);
|
|
||||||
await useAuth(req, res);
|
|
||||||
|
|
||||||
const { id: websiteId } = req.query;
|
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
if (!(await canViewWebsite(req.auth, websiteId))) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { startAt, endAt, eventName, urlPath, filters } = req.body;
|
|
||||||
|
|
||||||
const startDate = new Date(+startAt);
|
|
||||||
const endDate = new Date(+endAt);
|
|
||||||
|
|
||||||
const events = await getEventData(websiteId, {
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
eventName,
|
|
||||||
urlPath,
|
|
||||||
filters,
|
|
||||||
});
|
|
||||||
|
|
||||||
return ok(res, events);
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
|
||||||
};
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
import TestConsole from 'components/pages/console/TestConsole';
|
import TestConsole from 'components/pages/console/TestConsole';
|
||||||
|
|
||||||
export default function ConsolePage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
|
||||||
export default function DefaultPage() {
|
export default function () {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import LoginLayout from 'components/pages/login/LoginLayout';
|
import LoginLayout from 'components/pages/login/LoginLayout';
|
||||||
import LoginForm from 'components/pages/login/LoginForm';
|
import LoginForm from 'components/pages/login/LoginForm';
|
||||||
|
|
||||||
export default function LoginPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import useApi from 'hooks/useApi';
|
||||||
import { setUser } from 'store/app';
|
import { setUser } from 'store/app';
|
||||||
import { removeClientAuthToken } from 'lib/client';
|
import { removeClientAuthToken } from 'lib/client';
|
||||||
|
|
||||||
export default function LogoutPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { post } = useApi();
|
const { post } = useApi();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import { useRouter } from 'next/router';
|
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
|
||||||
import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard';
|
|
||||||
import useMessages from 'hooks/useMessages';
|
|
||||||
import useApi from 'hooks/useApi';
|
|
||||||
|
|
||||||
export default function RealtimeDetailsPage() {
|
|
||||||
const router = useRouter();
|
|
||||||
const { id: websiteId } = router.query;
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
const { get, useQuery } = useApi();
|
|
||||||
const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), {
|
|
||||||
enabled: !!websiteId,
|
|
||||||
});
|
|
||||||
const title = `${formatMessage(labels.realtime)}${website?.name ? ` - ${website.name}` : ''}`;
|
|
||||||
|
|
||||||
if (!websiteId) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppLayout title={title}>
|
|
||||||
<RealtimeDashboard key={websiteId} websiteId={websiteId} />
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
|
||||||
import RealtimeHome from 'components/pages/realtime/RealtimeHome';
|
|
||||||
import useMessages from 'hooks/useMessages';
|
|
||||||
|
|
||||||
export default function RealtimePage() {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
return (
|
|
||||||
<AppLayout title={formatMessage(labels.realtime)}>
|
|
||||||
<RealtimeHome />
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||||
import ReportDetails from 'components/pages/reports/ReportDetails';
|
import ReportDetails from 'components/pages/reports/ReportDetails';
|
||||||
import { useApi, useMessages } from 'hooks';
|
import { useApi, useMessages } from 'hooks';
|
||||||
|
|
||||||
export default function ReportsPage() {
|
export default function () {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||||
import ReportTemplates from 'components/pages/reports/ReportTemplates';
|
import ReportTemplates from 'components/pages/reports/ReportTemplates';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
|
|
||||||
export default function ReportsPage() {
|
export default function () {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||||
import EventDataReport from 'components/pages/reports/event-data/EventDataReport';
|
import EventDataReport from 'components/pages/reports/event-data/EventDataReport';
|
||||||
import { useMessages } from 'hooks';
|
import { useMessages } from 'hooks';
|
||||||
|
|
||||||
export default function Report() {
|
export default function () {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout';
|
||||||
import FunnelReport from 'components/pages/reports/funnel/FunnelReport';
|
import FunnelReport from 'components/pages/reports/funnel/FunnelReport';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function Funnel() {
|
export default function () {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
|
||||||
import useMessages from 'hooks/useMessages';
|
|
||||||
import ReportsList from 'components/pages/reports/ReportsList';
|
|
||||||
|
|
||||||
export default function ReportsPage() {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppLayout title={formatMessage(labels.reports)}>
|
|
||||||
<ReportsList />
|
|
||||||
</AppLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||||
import ProfileSettings from 'components/pages/settings/profile/ProfileSettings';
|
import ProfileSettings from 'components/pages/settings/profile/ProfileSettings';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function () {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
return (
|
return (
|
||||||
<AppLayout title={`${formatMessage(labels.settings)} - ${formatMessage(labels.profile)}`}>
|
<AppLayout title={`${formatMessage(labels.settings)} - ${formatMessage(labels.profile)}`}>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import TeamSettings from 'components/pages/settings/teams/TeamSettings';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function TeamDetailPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||||
import TeamsList from 'components/pages/settings/teams/TeamsList';
|
import TeamsList from 'components/pages/settings/teams/TeamsList';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function TeamsPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import UserSettings from 'components/pages/settings/users/UserSettings';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function TeamDetailPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||||
import UsersList from 'components/pages/settings/users/UsersList';
|
import UsersList from 'components/pages/settings/users/UsersList';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function UsersPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import WebsiteSettings from 'components/pages/settings/websites/WebsiteSettings'
|
||||||
import SettingsLayout from 'components/layout/SettingsLayout';
|
import SettingsLayout from 'components/layout/SettingsLayout';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function WebsiteSettingsPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout';
|
||||||
import WebsitesList from 'components/pages/settings/websites/WebsitesList';
|
import WebsitesList from 'components/pages/settings/websites/WebsitesList';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function WebsitesPage({ disabled }) {
|
export default function ({ disabled }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
if (disabled) {
|
if (disabled) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import ShareLayout from 'components/layout/ShareLayout';
|
import ShareLayout from 'components/layout/ShareLayout';
|
||||||
import WebsiteDetails from 'components/pages/websites/WebsiteDetails';
|
import WebsiteDetailsPage from 'components/pages/websites/WebsiteDetailsPage';
|
||||||
import useShareToken from 'hooks/useShareToken';
|
import useShareToken from 'hooks/useShareToken';
|
||||||
|
|
||||||
export default function SharePage() {
|
export default function () {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
const shareId = id?.[0];
|
const shareId = id?.[0];
|
||||||
|
|
@ -15,7 +15,7 @@ export default function SharePage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ShareLayout>
|
<ShareLayout>
|
||||||
<WebsiteDetails websiteId={shareToken.websiteId} />
|
<WebsiteDetailsPage websiteId={shareToken.websiteId} />
|
||||||
</ShareLayout>
|
</ShareLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Loading } from 'react-basics';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { setClientAuthToken } from 'lib/client';
|
import { setClientAuthToken } from 'lib/client';
|
||||||
|
|
||||||
export default function SingleSignOnPage() {
|
export default function () {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { token, url } = router.query;
|
const { token, url } = router.query;
|
||||||
|
|
||||||
|
|
|
||||||
20
pages/websites/[id]/event-data.js
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
|
import WebsiteEventDataPage from 'components/pages/websites/WebsiteEventDataPage';
|
||||||
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const router = useRouter();
|
||||||
|
const { id } = router.query;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout title={formatMessage(labels.websites)}>
|
||||||
|
<WebsiteEventDataPage websiteId={id} />
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import AppLayout from 'components/layout/AppLayout';
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
import WebsiteDetails from 'components/pages/websites/WebsiteDetails';
|
import WebsiteDetailsPage from 'components/pages/websites/WebsiteDetailsPage';
|
||||||
import useMessages from 'hooks/useMessages';
|
import useMessages from 'hooks/useMessages';
|
||||||
|
|
||||||
export default function DetailsPage() {
|
export default function () {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
@ -14,7 +14,7 @@ export default function DetailsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout title={formatMessage(labels.websites)}>
|
<AppLayout title={formatMessage(labels.websites)}>
|
||||||
<WebsiteDetails websiteId={id} />
|
<WebsiteDetailsPage websiteId={id} />
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
18
pages/websites/[id]/realtime.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
|
import RealtimePage from 'components/pages/realtime/RealtimePage';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id: websiteId } = router.query;
|
||||||
|
|
||||||
|
if (!websiteId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<RealtimePage websiteId={websiteId} />
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
18
pages/websites/[id]/reports.js
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
|
import AppLayout from 'components/layout/AppLayout';
|
||||||
|
import WebsiteReportsPage from 'components/pages/websites/WebsiteReportsPage';
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const router = useRouter();
|
||||||
|
const { id: websiteId } = router.query;
|
||||||
|
|
||||||
|
if (!websiteId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<WebsiteReportsPage websiteId={websiteId} />
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
public/images/browsers/android-webview.png
Normal file
|
After Width: | Height: | Size: 806 B |
BIN
public/images/browsers/android.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
public/images/browsers/aol.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/images/browsers/beaker.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/images/browsers/blackberry.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/browsers/brave.png
Normal file
|
After Width: | Height: | Size: 635 B |
BIN
public/images/browsers/chrome.png
Normal file
|
After Width: | Height: | Size: 819 B |
BIN
public/images/browsers/chromium-webview.png
Normal file
|
After Width: | Height: | Size: 680 B |
BIN
public/images/browsers/crios.png
Normal file
|
After Width: | Height: | Size: 819 B |
BIN
public/images/browsers/curl.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
public/images/browsers/edge-chromium.png
Normal file
|
After Width: | Height: | Size: 811 B |
BIN
public/images/browsers/edge-ios.png
Normal file
|
After Width: | Height: | Size: 811 B |
BIN
public/images/browsers/edge.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/images/browsers/facebook.png
Normal file
|
After Width: | Height: | Size: 2 KiB |