New page and referrer url filters.

This commit is contained in:
Mike Cao 2020-08-22 22:01:14 -07:00
parent 1d977875be
commit cf8ed13d1f
11 changed files with 133 additions and 50 deletions

View File

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import WebsiteChart from 'components/metrics/WebsiteChart'; import WebsiteChart from 'components/metrics/WebsiteChart';
import MetricsTable from 'components/metrics/MetricsTable';
import WorldMap from 'components/common/WorldMap'; import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page'; import Page from 'components/layout/Page';
import WebsiteHeader from 'components/metrics/WebsiteHeader'; import WebsiteHeader from 'components/metrics/WebsiteHeader';
@ -53,6 +52,17 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
}, },
]; ];
const tableProps = {
websiteId,
startDate,
endDate,
limit: 10,
onExpand: handleExpand,
websiteDomain: data?.domain,
};
const DetailsComponent = expand?.component;
async function loadData() { async function loadData() {
setData(await get(`/api/website/${websiteId}`)); setData(await get(`/api/website/${websiteId}`));
} }
@ -73,10 +83,6 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
setExpand(menuOptions.find(e => e.value === value)); setExpand(menuOptions.find(e => e.value === value));
} }
function getMetricLabel(type) {
return type === 'url' || type === 'referrer' ? 'Views' : 'Visitors';
}
useEffect(() => { useEffect(() => {
if (websiteId) { if (websiteId) {
loadData(); loadData();
@ -87,15 +93,6 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
return null; return null;
} }
const tableProps = {
websiteId,
startDate,
endDate,
limit: 10,
onExpand: handleExpand,
websiteDomain: data?.domain,
};
return ( return (
<Page> <Page>
<div className="row"> <div className="row">
@ -149,7 +146,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
selectedOption={expand.value} selectedOption={expand.value}
onMenuSelect={handleMenuSelect} onMenuSelect={handleMenuSelect}
> >
{expand.component({ ...tableProps, limit: false })} <DetailsComponent {...tableProps} limit={false} />
</MenuLayout> </MenuLayout>
)} )}
</Page> </Page>

View File

@ -0,0 +1,29 @@
import React from 'react';
import classNames from 'classnames';
import Button from './Button';
import styles from './ButtonGroup.module.css';
export default function ButtonGroup({
items = [],
selectedItem,
className,
size,
icon,
onClick = () => {},
}) {
return (
<div className={classNames(styles.group, className)}>
{items.map(item => (
<Button
key={item}
className={classNames(styles.button, { [styles.selected]: selectedItem === item })}
size={size}
icon={icon}
onClick={() => onClick(item)}
>
{item}
</Button>
))}
</div>
);
}

View File

@ -0,0 +1,25 @@
.group {
display: inline-flex;
border-radius: 4px;
overflow: hidden;
border: 1px solid var(--gray400);
}
.group .button {
border-radius: 0;
background: var(--gray50);
border-left: 1px solid var(--gray400);
padding: 4px 8px;
}
.group .button:first-child {
border: 0;
}
.group .button + .button {
margin: 0;
}
.selected {
font-weight: 600;
}

View File

@ -16,7 +16,7 @@ export default function Header() {
<div className="col-12 col-md-6"> <div className="col-12 col-md-6">
<div className={styles.title}> <div className={styles.title}>
<Icon icon={<Logo />} size="large" className={styles.logo} /> <Icon icon={<Logo />} size="large" className={styles.logo} />
{user ? <Link href="/">umami</Link> : 'umami'} <Link href={user ? '/' : 'https://umami.is'}>umami</Link>
</div> </div>
</div> </div>
{user && ( {user && (

View File

@ -38,7 +38,7 @@ export default function MetricsTable({
return items; return items;
} }
return []; return [];
}, [data]); }, [data, dataFilter, filterOptions]);
async function loadData() { async function loadData() {
const data = await get(`/api/website/${websiteId}/rankings`, { const data = await get(`/api/website/${websiteId}/rankings`, {

View File

@ -1,6 +1,7 @@
import React from 'react'; import React, { useState } from 'react';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { urlFilter } from 'lib/filters'; import { urlFilter } from 'lib/filters';
import ButtonGroup from '../common/ButtonGroup';
export default function PagesTable({ export default function PagesTable({
websiteId, websiteId,
@ -10,19 +11,32 @@ export default function PagesTable({
limit, limit,
onExpand, onExpand,
}) { }) {
const [filter, setFilter] = useState('Combined');
return ( return (
<MetricsTable <MetricsTable
title="Pages" title="Pages"
type="url" type="url"
metric="Views" metric="Views"
headerComponent={null} headerComponent={limit ? null : <FilterButtons selected={filter} onClick={setFilter} />}
websiteId={websiteId} websiteId={websiteId}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
limit={limit} limit={limit}
dataFilter={urlFilter} dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain }} filterOptions={{ domain: websiteDomain, raw: filter === 'Raw' }}
onExpand={onExpand} onExpand={onExpand}
/> />
); );
} }
const FilterButtons = ({ selected, onClick }) => {
return (
<ButtonGroup
size="xsmall"
items={['Combined', 'Raw']}
selectedItem={selected}
onClick={onClick}
/>
);
};

View File

@ -1,31 +1,28 @@
import React from 'react'; import React from 'react';
import classNames from 'classnames'; import ButtonGroup from 'components/common/ButtonGroup';
import Button from '../common/Button';
import { getDateRange } from 'lib/date'; import { getDateRange } from 'lib/date';
import styles from './QuickButtons.module.css'; import styles from './QuickButtons.module.css';
const options = { const options = {
'24hour': '24h', '24h': '24hour',
'7day': '7d', '7d': '7day',
'30day': '30d', '30d': '30day',
}; };
export default function QuickButtons({ value, onChange }) { export default function QuickButtons({ value, onChange }) {
const selectedItem = Object.keys(options).find(key => options[key] === value);
function handleClick(value) { function handleClick(value) {
onChange(getDateRange(value)); onChange(getDateRange(options[value]));
} }
return ( return (
<div className={styles.buttons}> <ButtonGroup
{Object.keys(options).map(key => ( size="xsmall"
<Button className={styles.buttons}
key={key} items={Object.keys(options)}
className={classNames(styles.button, { [styles.active]: value === key })} selectedItem={selectedItem}
onClick={() => handleClick(key)} onClick={handleClick}
> />
{options[key]}
</Button>
))}
</div>
); );
} }

View File

@ -1,6 +1,7 @@
import React from 'react'; import React, { useState } from 'react';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import { refFilter } from 'lib/filters'; import { refFilter } from 'lib/filters';
import ButtonGroup from '../common/ButtonGroup';
export default function Referrers({ export default function Referrers({
websiteId, websiteId,
@ -10,19 +11,36 @@ export default function Referrers({
limit, limit,
onExpand = () => {}, onExpand = () => {},
}) { }) {
const [filter, setFilter] = useState('Combined');
return ( return (
<MetricsTable <MetricsTable
title="Referrers" title="Referrers"
type="referrer" type="referrer"
metric="Views" metric="Views"
headerComponent={null} headerComponent={limit ? null : <FilterButtons selected={filter} onClick={setFilter} />}
websiteId={websiteId} websiteId={websiteId}
startDate={startDate} startDate={startDate}
endDate={endDate} endDate={endDate}
limit={limit} limit={limit}
dataFilter={refFilter} dataFilter={refFilter}
filterOptions={{ domain: websiteDomain }} filterOptions={{
domain: websiteDomain,
domainOnly: filter === 'Domain only',
raw: filter === 'Raw',
}}
onExpand={onExpand} onExpand={onExpand}
/> />
); );
} }
const FilterButtons = ({ selected, onClick }) => {
return (
<ButtonGroup
size="xsmall"
items={['Domain only', 'Combined', 'Raw']}
selectedItem={selected}
onClick={onClick}
/>
);
};

View File

@ -5,11 +5,15 @@ import { removeTrailingSlash } from './format';
export const browserFilter = data => export const browserFilter = data =>
data.map(({ x, ...props }) => ({ x: BROWSERS[x] || x, ...props })); data.map(({ x, ...props }) => ({ x: BROWSERS[x] || x, ...props }));
export const urlFilter = (data, { domain }) => { export const urlFilter = (data, { domain, raw }) => {
const isValidUrl = url => { const isValidUrl = url => {
return url !== '' && !url.startsWith('#'); return url !== '' && !url.startsWith('#');
}; };
if (raw) {
return data.filter(({ x }) => isValidUrl(x));
}
const cleanUrl = url => { const cleanUrl = url => {
try { try {
const { pathname, searchParams } = new URL(url); const { pathname, searchParams } = new URL(url);
@ -47,11 +51,16 @@ export const urlFilter = (data, { domain }) => {
.sort(firstBy('y', -1).thenBy('x')); .sort(firstBy('y', -1).thenBy('x'));
}; };
export const refFilter = (data, { domain, domainsOnly }) => { export const refFilter = (data, { domain, domainOnly, raw }) => {
const isValidRef = ref => { const isValidRef = ref => {
return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#'); return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#');
}; };
if (raw) {
const regex = new RegExp(`http[s]?://([^.]+.)?${domain}`);
return data.filter(({ x }) => isValidRef(x) && !regex.test(x));
}
const cleanUrl = url => { const cleanUrl = url => {
try { try {
const { hostname, origin, pathname, searchParams, protocol } = new URL(url); const { hostname, origin, pathname, searchParams, protocol } = new URL(url);
@ -60,7 +69,7 @@ export const refFilter = (data, { domain, domainsOnly }) => {
return null; return null;
} }
if (domainsOnly && hostname) { if (domainOnly && hostname) {
return hostname; return hostname;
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "umami", "name": "umami",
"version": "0.15.1", "version": "0.16.0",
"description": "A simple, fast, website analytics alternative to Google Analytics. ", "description": "A simple, fast, website analytics alternative to Google Analytics. ",
"author": "Mike Cao <mike@mikecao.com>", "author": "Mike Cao <mike@mikecao.com>",
"license": "MIT", "license": "MIT",
@ -49,7 +49,6 @@
"date-fns-tz": "^1.0.10", "date-fns-tz": "^1.0.10",
"detect-browser": "^5.1.1", "detect-browser": "^5.1.1",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"escape-string-regexp": "^4.0.0",
"formik": "^2.1.5", "formik": "^2.1.5",
"geolite2-redist": "^1.0.7", "geolite2-redist": "^1.0.7",
"is-localhost-ip": "^1.4.0", "is-localhost-ip": "^1.4.0",

View File

@ -3500,11 +3500,6 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
escape-string-regexp@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34"
integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
eslint-config-prettier@^6.11.0: eslint-config-prettier@^6.11.0:
version "6.11.0" version "6.11.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz#f6d2238c1290d01c859a8b5c1f7d352a0b0da8b1"