mirror of
https://github.com/kremalicious/umami.git
synced 2025-02-14 21:10:34 +01:00
Updated pages and referrer filters to merge urls.
This commit is contained in:
parent
e75593443a
commit
3a515b56b2
@ -14,6 +14,7 @@
|
|||||||
},
|
},
|
||||||
"plugins": ["react"],
|
"plugins": ["react"],
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"react/display-name": "off",
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"react/prop-types": "off"
|
"react/prop-types": "off"
|
||||||
},
|
},
|
||||||
|
@ -6,7 +6,7 @@ Umami is a simple, fast, website analytics alternative to Google Analytics.
|
|||||||
|
|
||||||
A detailed getting started guide can be found at [https://umami.is/docs/](https://umami.is/docs/)
|
A detailed getting started guide can be found at [https://umami.is/docs/](https://umami.is/docs/)
|
||||||
|
|
||||||
## Installation from source
|
## Installing from source
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ By default this will launch the application on `http://localhost:3000`. You will
|
|||||||
[proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server
|
[proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) requests from your web server
|
||||||
or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.
|
or change the [port](https://nextjs.org/docs/api-reference/cli#production) to serve the application directly.
|
||||||
|
|
||||||
## Installation with Docker
|
## Installing with Docker
|
||||||
|
|
||||||
To build the umami container and start up a Postgres database, run:
|
To build the umami container and start up a Postgres database, run:
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
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 RankingsChart from 'components/metrics/RankingsChart';
|
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';
|
||||||
@ -9,12 +9,14 @@ import MenuLayout from 'components/layout/MenuLayout';
|
|||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters';
|
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
import styles from './WebsiteDetails.module.css';
|
import styles from './WebsiteDetails.module.css';
|
||||||
|
import PagesTable from './metrics/PagesTable';
|
||||||
const pageviewClasses = 'col-md-12 col-lg-6';
|
import ReferrersTable from './metrics/ReferrersTable';
|
||||||
const sessionClasses = 'col-md-12 col-lg-4';
|
import BrowsersTable from './metrics/BrowsersTable';
|
||||||
|
import OSTable from './metrics/OSTable';
|
||||||
|
import DevicesTable from './metrics/DevicesTable';
|
||||||
|
import CountriesTable from './metrics/CountriesTable';
|
||||||
|
|
||||||
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
|
export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' }) {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
@ -24,9 +26,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
const [expand, setExpand] = useState();
|
const [expand, setExpand] = useState();
|
||||||
const { startDate, endDate } = dateRange;
|
const { startDate, endDate } = dateRange;
|
||||||
|
|
||||||
const menuOptions = [
|
const BackButton = () => (
|
||||||
{
|
|
||||||
render: () => (
|
|
||||||
<Button
|
<Button
|
||||||
className={styles.backButton}
|
className={styles.backButton}
|
||||||
icon={<Arrow />}
|
icon={<Arrow />}
|
||||||
@ -35,18 +35,21 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
>
|
>
|
||||||
<div>Back</div>
|
<div>Back</div>
|
||||||
</Button>
|
</Button>
|
||||||
),
|
);
|
||||||
|
|
||||||
|
const menuOptions = [
|
||||||
|
{
|
||||||
|
render: BackButton,
|
||||||
},
|
},
|
||||||
{ label: 'Pages', value: 'url', filter: urlFilter },
|
{ label: 'Pages', value: 'url', component: PagesTable },
|
||||||
{ label: 'Referrers', value: 'referrer', filter: refFilter(data?.domain) },
|
{ label: 'Referrers', value: 'referrer', component: ReferrersTable },
|
||||||
{ label: 'Browsers', value: 'browser', filter: browserFilter },
|
{ label: 'Browsers', value: 'browser', component: BrowsersTable },
|
||||||
{ label: 'Operating system', value: 'os' },
|
{ label: 'Operating system', value: 'os', component: OSTable },
|
||||||
{ label: 'Devices', value: 'device', filter: deviceFilter },
|
{ label: 'Devices', value: 'device', component: DevicesTable },
|
||||||
{
|
{
|
||||||
label: 'Countries',
|
label: 'Countries',
|
||||||
value: 'country',
|
value: 'country',
|
||||||
filter: countryFilter,
|
component: props => <CountriesTable {...props} onDataLoad={data => setCountryData(data)} />,
|
||||||
onDataLoad: data => setCountryData(data),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -70,7 +73,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
setExpand(menuOptions.find(e => e.value === value));
|
setExpand(menuOptions.find(e => e.value === value));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHeading(type) {
|
function getMetricLabel(type) {
|
||||||
return type === 'url' || type === 'referrer' ? 'Views' : 'Visitors';
|
return type === 'url' || type === 'referrer' ? 'Views' : 'Visitors';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -84,6 +87,15 @@ 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">
|
||||||
@ -100,71 +112,22 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
{chartLoaded && !expand && (
|
{chartLoaded && !expand && (
|
||||||
<>
|
<>
|
||||||
<div className={classNames(styles.row, 'row')}>
|
<div className={classNames(styles.row, 'row')}>
|
||||||
<div className={pageviewClasses}>
|
<div className="col-md-12 col-lg-6">
|
||||||
<RankingsChart
|
<PagesTable {...tableProps} />
|
||||||
title="Pages"
|
|
||||||
type="url"
|
|
||||||
heading="Views"
|
|
||||||
websiteId={websiteId}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
limit={10}
|
|
||||||
dataFilter={urlFilter}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={pageviewClasses}>
|
<div className="col-md-12 col-lg-6">
|
||||||
<RankingsChart
|
<ReferrersTable {...tableProps} />
|
||||||
title="Referrers"
|
|
||||||
type="referrer"
|
|
||||||
heading="Views"
|
|
||||||
websiteId={websiteId}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
limit={10}
|
|
||||||
dataFilter={refFilter(data.domain)}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames(styles.row, 'row')}>
|
<div className={classNames(styles.row, 'row')}>
|
||||||
<div className={sessionClasses}>
|
<div className="col-md-12 col-lg-4">
|
||||||
<RankingsChart
|
<BrowsersTable {...tableProps} />
|
||||||
title="Browsers"
|
|
||||||
type="browser"
|
|
||||||
heading="Visitors"
|
|
||||||
websiteId={websiteId}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
limit={10}
|
|
||||||
dataFilter={browserFilter}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={sessionClasses}>
|
<div className="col-md-12 col-lg-4">
|
||||||
<RankingsChart
|
<OSTable {...tableProps} />
|
||||||
title="Operating system"
|
|
||||||
type="os"
|
|
||||||
heading="Visitors"
|
|
||||||
websiteId={websiteId}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
limit={10}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className={sessionClasses}>
|
<div className="col-md-12 col-lg-4">
|
||||||
<RankingsChart
|
<DevicesTable {...tableProps} />
|
||||||
title="Devices"
|
|
||||||
type="device"
|
|
||||||
heading="Visitors"
|
|
||||||
websiteId={websiteId}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
limit={10}
|
|
||||||
dataFilter={deviceFilter}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={classNames(styles.row, 'row')}>
|
<div className={classNames(styles.row, 'row')}>
|
||||||
@ -172,18 +135,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
<WorldMap data={countryData} />
|
<WorldMap data={countryData} />
|
||||||
</div>
|
</div>
|
||||||
<div className="col-12 col-md-12 col-lg-4">
|
<div className="col-12 col-md-12 col-lg-4">
|
||||||
<RankingsChart
|
<CountriesTable {...tableProps} onDataLoad={data => setCountryData(data)} />
|
||||||
title="Countries"
|
|
||||||
type="country"
|
|
||||||
heading="Visitors"
|
|
||||||
websiteId={websiteId}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
limit={10}
|
|
||||||
dataFilter={countryFilter}
|
|
||||||
onDataLoad={data => setCountryData(data)}
|
|
||||||
onExpand={handleExpand}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -197,16 +149,7 @@ export default function WebsiteDetails({ websiteId, defaultDateRange = '7day' })
|
|||||||
selectedOption={expand.value}
|
selectedOption={expand.value}
|
||||||
onMenuSelect={handleMenuSelect}
|
onMenuSelect={handleMenuSelect}
|
||||||
>
|
>
|
||||||
<RankingsChart
|
{expand.component({ ...tableProps, limit: false })}
|
||||||
title={expand.label}
|
|
||||||
type={expand.value}
|
|
||||||
heading={getHeading(expand.value)}
|
|
||||||
websiteId={websiteId}
|
|
||||||
startDate={startDate}
|
|
||||||
endDate={endDate}
|
|
||||||
dataFilter={expand.filter}
|
|
||||||
onDataLoad={expand.onDataLoad}
|
|
||||||
/>
|
|
||||||
</MenuLayout>
|
</MenuLayout>
|
||||||
)}
|
)}
|
||||||
</Page>
|
</Page>
|
||||||
|
@ -17,10 +17,6 @@
|
|||||||
background: #eaeaea;
|
background: #eaeaea;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button + .button {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.large {
|
.large {
|
||||||
font-size: var(--font-size-large);
|
font-size: var(--font-size-large);
|
||||||
}
|
}
|
||||||
|
7
components/layout/ButtonLayout.js
Normal file
7
components/layout/ButtonLayout.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import styles from './ButtonLayout.module.css';
|
||||||
|
|
||||||
|
export default function ButtonLayout({ className, children }) {
|
||||||
|
return <div className={classNames(styles.buttons, className)}>{children}</div>;
|
||||||
|
}
|
7
components/layout/ButtonLayout.module.css
Normal file
7
components/layout/ButtonLayout.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons button + button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
@ -23,6 +23,10 @@
|
|||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.buttons button + button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
19
components/metrics/BrowsersTable.js
Normal file
19
components/metrics/BrowsersTable.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
import { browserFilter } from 'lib/filters';
|
||||||
|
|
||||||
|
export default function BrowsersTable({ websiteId, startDate, endDate, limit, onExpand }) {
|
||||||
|
return (
|
||||||
|
<MetricsTable
|
||||||
|
title="Browsers"
|
||||||
|
type="browser"
|
||||||
|
metric="Visitors"
|
||||||
|
websiteId={websiteId}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
limit={limit}
|
||||||
|
dataFilter={browserFilter}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
27
components/metrics/CountriesTable.js
Normal file
27
components/metrics/CountriesTable.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
import { countryFilter, percentFilter } from 'lib/filters';
|
||||||
|
|
||||||
|
export default function CountriesTable({
|
||||||
|
websiteId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
limit,
|
||||||
|
onDataLoad,
|
||||||
|
onExpand,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MetricsTable
|
||||||
|
title="Countries"
|
||||||
|
type="country"
|
||||||
|
metric="Visitors"
|
||||||
|
websiteId={websiteId}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
limit={limit}
|
||||||
|
dataFilter={countryFilter}
|
||||||
|
onDataLoad={data => onDataLoad(percentFilter(data))}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
19
components/metrics/DevicesTable.js
Normal file
19
components/metrics/DevicesTable.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
import { deviceFilter } from 'lib/filters';
|
||||||
|
|
||||||
|
export default function DevicesTable({ websiteId, startDate, endDate, limit, onExpand }) {
|
||||||
|
return (
|
||||||
|
<MetricsTable
|
||||||
|
title="Devices"
|
||||||
|
type="device"
|
||||||
|
metric="Visitors"
|
||||||
|
websiteId={websiteId}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
limit={limit}
|
||||||
|
dataFilter={deviceFilter}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -7,18 +7,20 @@ import Arrow from 'assets/arrow-right.svg';
|
|||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { percentFilter } from 'lib/filters';
|
import { percentFilter } from 'lib/filters';
|
||||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import styles from './RankingsChart.module.css';
|
import styles from './MetricsTable.module.css';
|
||||||
|
|
||||||
export default function RankingsChart({
|
export default function MetricsTable({
|
||||||
title,
|
title,
|
||||||
|
metric,
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
type,
|
type,
|
||||||
heading,
|
|
||||||
className,
|
className,
|
||||||
dataFilter,
|
dataFilter,
|
||||||
|
filterOptions,
|
||||||
limit,
|
limit,
|
||||||
|
headerComponent,
|
||||||
onDataLoad = () => {},
|
onDataLoad = () => {},
|
||||||
onExpand = () => {},
|
onExpand = () => {},
|
||||||
}) {
|
}) {
|
||||||
@ -29,7 +31,7 @@ export default function RankingsChart({
|
|||||||
|
|
||||||
const rankings = useMemo(() => {
|
const rankings = useMemo(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
const items = dataFilter ? dataFilter(data) : data;
|
const items = percentFilter(dataFilter ? dataFilter(data, filterOptions) : data);
|
||||||
if (limit) {
|
if (limit) {
|
||||||
return items.filter((e, i) => i < limit);
|
return items.filter((e, i) => i < limit);
|
||||||
}
|
}
|
||||||
@ -45,10 +47,8 @@ export default function RankingsChart({
|
|||||||
type,
|
type,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updated = percentFilter(data);
|
setData(data);
|
||||||
|
onDataLoad(data);
|
||||||
setData(updated);
|
|
||||||
onDataLoad(updated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSetFormat() {
|
function handleSetFormat() {
|
||||||
@ -88,8 +88,9 @@ export default function RankingsChart({
|
|||||||
<div className={classNames(styles.container, className)}>
|
<div className={classNames(styles.container, className)}>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div className={styles.title}>{title}</div>
|
<div className={styles.title}>{title}</div>
|
||||||
<div className={styles.heading} onClick={handleSetFormat}>
|
{headerComponent}
|
||||||
{heading}
|
<div className={styles.metric} onClick={handleSetFormat}>
|
||||||
|
{metric}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.body}>
|
<div className={styles.body}>
|
||||||
@ -121,9 +122,11 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.row} onClick={onClick}>
|
<div className={styles.row}>
|
||||||
<div className={styles.label}>{label}</div>
|
<div className={styles.label}>{decodeURI(label)}</div>
|
||||||
|
<div className={styles.value} onClick={onClick}>
|
||||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||||
|
</div>
|
||||||
<div className={styles.percent}>
|
<div className={styles.percent}>
|
||||||
<animated.div
|
<animated.div
|
||||||
className={styles.bar}
|
className={styles.bar}
|
@ -9,16 +9,18 @@
|
|||||||
|
|
||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
line-height: 40px;
|
line-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
flex: 1;
|
display: flex;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: var(--font-size-normal);
|
font-size: var(--font-size-normal);
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
.metric {
|
||||||
font-size: var(--font-size-small);
|
font-size: var(--font-size-small);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100px;
|
width: 100px;
|
17
components/metrics/OSTable.js
Normal file
17
components/metrics/OSTable.js
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
|
||||||
|
export default function OSTable({ websiteId, startDate, endDate, limit, onExpand }) {
|
||||||
|
return (
|
||||||
|
<MetricsTable
|
||||||
|
title="Operating System"
|
||||||
|
type="os"
|
||||||
|
metric="Visitors"
|
||||||
|
websiteId={websiteId}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
limit={limit}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
28
components/metrics/PagesTable.js
Normal file
28
components/metrics/PagesTable.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
import { urlFilter } from 'lib/filters';
|
||||||
|
|
||||||
|
export default function PagesTable({
|
||||||
|
websiteId,
|
||||||
|
websiteDomain,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
limit,
|
||||||
|
onExpand,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MetricsTable
|
||||||
|
title="Pages"
|
||||||
|
type="url"
|
||||||
|
metric="Views"
|
||||||
|
headerComponent={null}
|
||||||
|
websiteId={websiteId}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
limit={limit}
|
||||||
|
dataFilter={urlFilter}
|
||||||
|
filterOptions={{ domain: websiteDomain }}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
28
components/metrics/ReferrersTable.js
Normal file
28
components/metrics/ReferrersTable.js
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import MetricsTable from './MetricsTable';
|
||||||
|
import { refFilter } from 'lib/filters';
|
||||||
|
|
||||||
|
export default function Referrers({
|
||||||
|
websiteId,
|
||||||
|
websiteDomain,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
limit,
|
||||||
|
onExpand = () => {},
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<MetricsTable
|
||||||
|
title="Referrers"
|
||||||
|
type="referrer"
|
||||||
|
metric="Views"
|
||||||
|
headerComponent={null}
|
||||||
|
websiteId={websiteId}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
limit={limit}
|
||||||
|
dataFilter={refFilter}
|
||||||
|
filterOptions={{ domain: websiteDomain }}
|
||||||
|
onExpand={onExpand}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -6,6 +6,7 @@ import Icon from 'components/common/Icon';
|
|||||||
import Table from 'components/common/Table';
|
import Table from 'components/common/Table';
|
||||||
import Modal from 'components/common/Modal';
|
import Modal from 'components/common/Modal';
|
||||||
import AccountEditForm from 'components/forms/AccountEditForm';
|
import AccountEditForm from 'components/forms/AccountEditForm';
|
||||||
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import Pen from 'assets/pen.svg';
|
import Pen from 'assets/pen.svg';
|
||||||
import Plus from 'assets/plus.svg';
|
import Plus from 'assets/plus.svg';
|
||||||
import Trash from 'assets/trash.svg';
|
import Trash from 'assets/trash.svg';
|
||||||
@ -25,14 +26,14 @@ export default function AccountSettings() {
|
|||||||
|
|
||||||
const Buttons = row =>
|
const Buttons = row =>
|
||||||
row.username !== 'admin' ? (
|
row.username !== 'admin' ? (
|
||||||
<>
|
<ButtonLayout>
|
||||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||||
<div>Edit</div>
|
<div>Edit</div>
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
||||||
<div>Delete</div>
|
<div>Delete</div>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</ButtonLayout>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
@ -9,6 +9,7 @@ import DeleteForm from '../forms/DeleteForm';
|
|||||||
import TrackingCodeForm from '../forms/TrackingCodeForm';
|
import TrackingCodeForm from '../forms/TrackingCodeForm';
|
||||||
import ShareUrlForm from '../forms/ShareUrlForm';
|
import ShareUrlForm from '../forms/ShareUrlForm';
|
||||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import Pen from 'assets/pen.svg';
|
import Pen from 'assets/pen.svg';
|
||||||
import Trash from 'assets/trash.svg';
|
import Trash from 'assets/trash.svg';
|
||||||
import Plus from 'assets/plus.svg';
|
import Plus from 'assets/plus.svg';
|
||||||
@ -27,7 +28,7 @@ export default function WebsiteSettings() {
|
|||||||
const [saved, setSaved] = useState(0);
|
const [saved, setSaved] = useState(0);
|
||||||
|
|
||||||
const Buttons = row => (
|
const Buttons = row => (
|
||||||
<>
|
<ButtonLayout>
|
||||||
{row.share_id && (
|
{row.share_id && (
|
||||||
<Button
|
<Button
|
||||||
icon={<Link />}
|
icon={<Link />}
|
||||||
@ -50,7 +51,7 @@ export default function WebsiteSettings() {
|
|||||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
|
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
|
||||||
<div>Delete</div>
|
<div>Delete</div>
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</ButtonLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
|
106
lib/filters.js
106
lib/filters.js
@ -1,16 +1,108 @@
|
|||||||
import escape from 'escape-string-regexp';
|
import firstBy from 'thenby';
|
||||||
import { BROWSERS, ISO_COUNTRIES, DEVICES } from './constants';
|
import { BROWSERS, ISO_COUNTRIES, DEVICES } from './constants';
|
||||||
|
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 => data.filter(({ x }) => x !== '' && !x.startsWith('#'));
|
export const urlFilter = (data, { domain }) => {
|
||||||
|
const isValidUrl = url => {
|
||||||
|
return url !== '' && !url.startsWith('#');
|
||||||
|
};
|
||||||
|
|
||||||
export const refFilter = domain => data => {
|
const cleanUrl = url => {
|
||||||
const regex = new RegExp(escape(domain));
|
try {
|
||||||
return data.filter(
|
const { pathname, searchParams } = new URL(url);
|
||||||
({ x }) => x !== '' && !x.startsWith('/') && !x.startsWith('#') && !regex.test(x),
|
|
||||||
);
|
const path = removeTrailingSlash(pathname);
|
||||||
|
const ref = searchParams.get('ref');
|
||||||
|
const query = ref ? `?ref=${ref}` : '';
|
||||||
|
|
||||||
|
return `${path}${query}`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = data.reduce((obj, { x, y }) => {
|
||||||
|
if (!isValidUrl(x)) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = cleanUrl(x.startsWith('/') ? `http://${domain}${x}` : x);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
if (!obj[url]) {
|
||||||
|
obj[url] = y;
|
||||||
|
} else {
|
||||||
|
obj[url] += y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return Object.keys(map)
|
||||||
|
.map(key => ({ x: key, y: map[key] }))
|
||||||
|
.sort(firstBy('y', -1).thenBy('x'));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const refFilter = (data, { domain, domainsOnly }) => {
|
||||||
|
const isValidRef = ref => {
|
||||||
|
return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#');
|
||||||
|
};
|
||||||
|
|
||||||
|
const cleanUrl = url => {
|
||||||
|
try {
|
||||||
|
const { hostname, origin, pathname, searchParams, protocol } = new URL(url);
|
||||||
|
|
||||||
|
if (hostname === domain) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainsOnly && hostname) {
|
||||||
|
return hostname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!origin || origin === 'null') {
|
||||||
|
return `${protocol}${removeTrailingSlash(pathname)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol.startsWith('http')) {
|
||||||
|
const path = removeTrailingSlash(pathname);
|
||||||
|
const ref = searchParams.get('ref');
|
||||||
|
const query = ref ? `?ref=${ref}` : '';
|
||||||
|
|
||||||
|
return `${origin}${path}${query}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const map = data.reduce((obj, { x, y }) => {
|
||||||
|
if (!isValidRef(x)) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = cleanUrl(x);
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
if (!obj[url]) {
|
||||||
|
obj[url] = y;
|
||||||
|
} else {
|
||||||
|
obj[url] += y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return Object.keys(map)
|
||||||
|
.map(key => ({ x: key, y: map[key] }))
|
||||||
|
.sort(firstBy('y', -1).thenBy('x'));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deviceFilter = data =>
|
export const deviceFilter = data =>
|
||||||
|
@ -62,3 +62,7 @@ export function formatLongNumber(value) {
|
|||||||
|
|
||||||
return formatNumber(n);
|
return formatNumber(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeTrailingSlash(url) {
|
||||||
|
return url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "0.14.0",
|
"version": "0.15.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",
|
||||||
@ -68,6 +68,7 @@
|
|||||||
"redux": "^4.0.5",
|
"redux": "^4.0.5",
|
||||||
"redux-thunk": "^2.3.0",
|
"redux-thunk": "^2.3.0",
|
||||||
"request-ip": "^2.1.3",
|
"request-ip": "^2.1.3",
|
||||||
|
"thenby": "^1.3.4",
|
||||||
"tinycolor2": "^1.4.1",
|
"tinycolor2": "^1.4.1",
|
||||||
"unfetch": "^4.1.0",
|
"unfetch": "^4.1.0",
|
||||||
"uuid": "^8.3.0"
|
"uuid": "^8.3.0"
|
||||||
|
@ -8316,6 +8316,11 @@ text-table@^0.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||||
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=
|
||||||
|
|
||||||
|
thenby@^1.3.4:
|
||||||
|
version "1.3.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/thenby/-/thenby-1.3.4.tgz#81581f6e1bb324c6dedeae9bfc28e59b1a2201cc"
|
||||||
|
integrity sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==
|
||||||
|
|
||||||
through2@^2.0.0:
|
through2@^2.0.0:
|
||||||
version "2.0.5"
|
version "2.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user