mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
New page and referrer url filters.
This commit is contained in:
parent
1d977875be
commit
cf8ed13d1f
@ -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>
|
||||||
|
29
components/common/ButtonGroup.js
Normal file
29
components/common/ButtonGroup.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
25
components/common/ButtonGroup.module.css
Normal file
25
components/common/ButtonGroup.module.css
Normal 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;
|
||||||
|
}
|
@ -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 && (
|
||||||
|
@ -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`, {
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user