mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 15:23:38 +01:00
URL filter functionality.
This commit is contained in:
parent
6bc371352c
commit
4fded49b03
@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useRouter } from 'next/router';
|
||||
import classNames from 'classnames';
|
||||
import WebsiteChart from 'components/metrics/WebsiteChart';
|
||||
import WorldMap from 'components/common/WorldMap';
|
||||
@ -19,6 +18,7 @@ import EventsTable from './metrics/EventsTable';
|
||||
import EventsChart from './metrics/EventsChart';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import Loading from 'components/common/Loading';
|
||||
import usePageQuery from '../hooks/usePageQuery';
|
||||
|
||||
const views = {
|
||||
url: PagesTable,
|
||||
@ -31,18 +31,16 @@ const views = {
|
||||
};
|
||||
|
||||
export default function WebsiteDetails({ websiteId, token }) {
|
||||
const router = useRouter();
|
||||
const { data } = useFetch(`/api/website/${websiteId}`, { token });
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
const [countryData, setCountryData] = useState();
|
||||
const [eventsData, setEventsData] = useState();
|
||||
const {
|
||||
query: { id, view },
|
||||
basePath,
|
||||
asPath,
|
||||
} = router;
|
||||
|
||||
const path = `${basePath}/${asPath.split('/')[1]}/${id.join('/')}`;
|
||||
pathname,
|
||||
resolve,
|
||||
router,
|
||||
query: { view },
|
||||
} = usePageQuery();
|
||||
|
||||
const BackButton = () => (
|
||||
<Button
|
||||
@ -50,11 +48,9 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
className={styles.backButton}
|
||||
icon={<Arrow />}
|
||||
size="xsmall"
|
||||
onClick={() => router.push(path)}
|
||||
onClick={() => router.push(pathname)}
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage id="button.back" defaultMessage="Back" />
|
||||
</div>
|
||||
<FormattedMessage id="button.back" defaultMessage="Back" />
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -64,31 +60,31 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
|
||||
value: `${path}?view=url`,
|
||||
value: resolve({ view: 'url' }),
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
|
||||
value: `${path}?view=referrer`,
|
||||
value: resolve({ view: 'referrer' }),
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />,
|
||||
value: `${path}?view=browser`,
|
||||
value: resolve({ view: 'browser' }),
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />,
|
||||
value: `${path}?view=os`,
|
||||
value: resolve({ view: 'os' }),
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.devices" defaultMessage="Devices" />,
|
||||
value: `${path}?view=device`,
|
||||
value: resolve({ view: 'device' }),
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.countries" defaultMessage="Countries" />,
|
||||
value: `${path}?view=country`,
|
||||
value: resolve({ view: 'country' }),
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
|
||||
value: `${path}?view=event`,
|
||||
value: resolve({ view: 'event' }),
|
||||
},
|
||||
];
|
||||
|
||||
@ -109,7 +105,7 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
}
|
||||
|
||||
function handleExpand(value) {
|
||||
router.push(`${path}?view=${value}`);
|
||||
router.push(resolve({ view: value }));
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
@ -179,7 +175,7 @@ export default function WebsiteDetails({ websiteId, token }) {
|
||||
contentClassName={styles.content}
|
||||
menu={menuOptions}
|
||||
>
|
||||
<DetailsComponent {...tableProps} limit={false} />
|
||||
<DetailsComponent {...tableProps} limit={false} showFilters={true} />
|
||||
</MenuLayout>
|
||||
)}
|
||||
</Page>
|
||||
|
@ -34,9 +34,7 @@ export default function WebsiteList({ userId }) {
|
||||
}
|
||||
>
|
||||
<Button icon={<Arrow />} size="medium" onClick={() => router.push('/settings')}>
|
||||
<div>
|
||||
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
|
||||
</div>
|
||||
<FormattedMessage id="message.go-to-settings" defaultMessage="Go to settings" />
|
||||
</Button>
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
|
@ -13,7 +13,8 @@ export default function Button({
|
||||
className,
|
||||
tooltip,
|
||||
tooltipId,
|
||||
disabled = false,
|
||||
disabled,
|
||||
iconRight,
|
||||
onClick = () => {},
|
||||
...props
|
||||
}) {
|
||||
@ -30,14 +31,14 @@ export default function Button({
|
||||
[styles.action]: variant === 'action',
|
||||
[styles.danger]: variant === 'danger',
|
||||
[styles.light]: variant === 'light',
|
||||
[styles.disabled]: disabled,
|
||||
[styles.iconRight]: iconRight,
|
||||
})}
|
||||
disabled={disabled}
|
||||
onClick={!disabled ? onClick : null}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon icon={icon} size={size} />}
|
||||
{children}
|
||||
{icon && <Icon className={styles.icon} icon={icon} size={size} />}
|
||||
{children && <div>{children}</div>}
|
||||
{tooltip && <ReactTooltip id={tooltipId}>{tooltip}</ReactTooltip>}
|
||||
</button>
|
||||
);
|
||||
|
@ -38,7 +38,8 @@
|
||||
font-size: var(--font-size-xsmall);
|
||||
}
|
||||
|
||||
.action {
|
||||
.action,
|
||||
.action:active {
|
||||
color: var(--gray50);
|
||||
background: var(--gray900);
|
||||
}
|
||||
@ -64,6 +65,19 @@
|
||||
background: inherit;
|
||||
}
|
||||
|
||||
.button .icon + div {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.button.iconRight .icon {
|
||||
order: 1;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.button.iconRight .icon + div {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
cursor: default;
|
||||
color: var(--gray500);
|
||||
|
@ -5,10 +5,6 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.icon + * {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.icon svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
@ -6,11 +6,13 @@ import useFetch from 'hooks/useFetch';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
import { EVENT_COLORS } from 'lib/constants';
|
||||
import usePageQuery from '../../hooks/usePageQuery';
|
||||
|
||||
export default function EventsChart({ websiteId, token }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const { query } = usePageQuery();
|
||||
|
||||
const { data } = useFetch(
|
||||
`/api/website/${websiteId}/events`,
|
||||
@ -19,6 +21,7 @@ export default function EventsChart({ websiteId, token }) {
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url: query.url,
|
||||
token,
|
||||
},
|
||||
{ update: [modified] },
|
||||
|
@ -5,24 +5,30 @@ import Loading from 'components/common/Loading';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import MetricCard from './MetricCard';
|
||||
import styles from './MetricsBar.module.css';
|
||||
|
||||
export default function MetricsBar({ websiteId, token, className }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const [format, setFormat] = useState(true);
|
||||
const {
|
||||
query: { url },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data } = useFetch(
|
||||
`/api/website/${websiteId}/metrics`,
|
||||
{
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
url,
|
||||
token,
|
||||
},
|
||||
{
|
||||
update: [modified],
|
||||
},
|
||||
);
|
||||
const [format, setFormat] = useState(true);
|
||||
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
|
||||
|
@ -12,6 +12,7 @@ import { percentFilter } from 'lib/filters';
|
||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import styles from './MetricsTable.module.css';
|
||||
import usePageQuery from '../../hooks/usePageQuery';
|
||||
|
||||
export default function MetricsTable({
|
||||
websiteId,
|
||||
@ -30,6 +31,10 @@ export default function MetricsTable({
|
||||
}) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, modified } = dateRange;
|
||||
const {
|
||||
query: { url },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data } = useFetch(
|
||||
`/api/website/${websiteId}/rankings`,
|
||||
{
|
||||
@ -37,6 +42,7 @@ export default function MetricsTable({
|
||||
start_at: +startDate,
|
||||
end_at: +endDate,
|
||||
domain: websiteDomain,
|
||||
url,
|
||||
token,
|
||||
},
|
||||
{ onDataLoad, delay: 300, update: [modified] },
|
||||
@ -101,9 +107,7 @@ export default function MetricsTable({
|
||||
<div className={styles.footer}>
|
||||
{limit && (
|
||||
<Button icon={<Arrow />} size="xsmall" onClick={() => onExpand(type)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.more" defaultMessage="More" />
|
||||
</div>
|
||||
<FormattedMessage id="button.more" defaultMessage="More" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -1,13 +1,23 @@
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Link from 'next/link';
|
||||
import ButtonGroup from 'components/common/ButtonGroup';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import { urlFilter } from 'lib/filters';
|
||||
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import MetricsTable from './MetricsTable';
|
||||
import ButtonLayout from '../layout/ButtonLayout';
|
||||
|
||||
export default function PagesTable({ websiteId, token, websiteDomain, limit, onExpand }) {
|
||||
export default function PagesTable({
|
||||
websiteId,
|
||||
token,
|
||||
websiteDomain,
|
||||
limit,
|
||||
showFilters,
|
||||
onExpand,
|
||||
}) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
const { resolve } = usePageQuery();
|
||||
|
||||
const buttons = [
|
||||
{
|
||||
@ -17,9 +27,17 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, onE
|
||||
{ label: <FormattedMessage id="metrics.filter.raw" defaultMessage="Raw" />, value: FILTER_RAW },
|
||||
];
|
||||
|
||||
const renderLink = ({ x }) => {
|
||||
return (
|
||||
<Link href={resolve({ url: x })} replace={true}>
|
||||
<a>{decodeURI(x)}</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!limit && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
|
||||
{showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
|
||||
<MetricsTable
|
||||
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
|
||||
type="url"
|
||||
@ -29,7 +47,7 @@ export default function PagesTable({ websiteId, token, websiteDomain, limit, onE
|
||||
limit={limit}
|
||||
dataFilter={urlFilter}
|
||||
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}
|
||||
renderLabel={({ x }) => decodeURI(x)}
|
||||
renderLabel={renderLink}
|
||||
onExpand={onExpand}
|
||||
/>
|
||||
</>
|
||||
|
@ -11,6 +11,7 @@ export default function ReferrersTable({
|
||||
websiteDomain,
|
||||
token,
|
||||
limit,
|
||||
showFilters,
|
||||
onExpand = () => {},
|
||||
}) {
|
||||
const [filter, setFilter] = useState(FILTER_COMBINED);
|
||||
@ -39,7 +40,7 @@ export default function ReferrersTable({
|
||||
|
||||
return (
|
||||
<>
|
||||
{!limit && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
|
||||
{showFilters && <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />}
|
||||
<MetricsTable
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
type="referrer"
|
||||
|
@ -5,10 +5,13 @@ import MetricsBar from './MetricsBar';
|
||||
import WebsiteHeader from './WebsiteHeader';
|
||||
import DateFilter from 'components/common/DateFilter';
|
||||
import StickyHeader from 'components/helpers/StickyHeader';
|
||||
import Button from 'components/common/Button';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
import useDateRange from 'hooks/useDateRange';
|
||||
import useTimezone from 'hooks/useTimezone';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { getDateArray, getDateLength } from 'lib/date';
|
||||
import Times from 'assets/times.svg';
|
||||
import styles from './WebsiteChart.module.css';
|
||||
|
||||
export default function WebsiteChart({
|
||||
@ -22,6 +25,11 @@ export default function WebsiteChart({
|
||||
const [dateRange, setDateRange] = useDateRange(websiteId);
|
||||
const { startDate, endDate, unit, value, modified } = dateRange;
|
||||
const [timezone] = useTimezone();
|
||||
const {
|
||||
router,
|
||||
resolve,
|
||||
query: { url },
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, loading } = useFetch(
|
||||
`/api/website/${websiteId}/pageviews`,
|
||||
@ -30,6 +38,7 @@ export default function WebsiteChart({
|
||||
end_at: +endDate,
|
||||
unit,
|
||||
tz: timezone,
|
||||
url,
|
||||
token,
|
||||
},
|
||||
{ onDataLoad, update: [modified] },
|
||||
@ -45,6 +54,10 @@ export default function WebsiteChart({
|
||||
return [[], []];
|
||||
}, [data]);
|
||||
|
||||
function handleCloseFilter() {
|
||||
router.push(resolve({ url: undefined }));
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} />
|
||||
@ -54,6 +67,7 @@ export default function WebsiteChart({
|
||||
stickyClassName={styles.sticky}
|
||||
enabled={stickyHeader}
|
||||
>
|
||||
{url && <PageFilter url={url} onClick={handleCloseFilter} />}
|
||||
<div className="col-12 col-lg-9">
|
||||
<MetricsBar websiteId={websiteId} token={token} />
|
||||
</div>
|
||||
@ -81,3 +95,13 @@ export default function WebsiteChart({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const PageFilter = ({ url, onClick }) => {
|
||||
return (
|
||||
<div className={classNames(styles.url, 'col-12')}>
|
||||
<Button icon={<Times />} onClick={onClick} variant="action" iconRight={true}>
|
||||
{url}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -36,6 +36,11 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.url {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.filter {
|
||||
display: block;
|
||||
|
@ -42,14 +42,10 @@ export default function AccountSettings() {
|
||||
row.username !== 'admin' ? (
|
||||
<ButtonLayout align="right">
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
</div>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
</div>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
) : null;
|
||||
@ -102,9 +98,7 @@ export default function AccountSettings() {
|
||||
<FormattedMessage id="label.accounts" defaultMessage="Accounts" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.add-account" defaultMessage="Add account" />
|
||||
</div>
|
||||
<FormattedMessage id="button.add-account" defaultMessage="Add account" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} />
|
||||
|
@ -29,9 +29,7 @@ export default function ProfileSettings() {
|
||||
<FormattedMessage id="label.profile" defaultMessage="Profile" />
|
||||
</div>
|
||||
<Button icon={<Dots />} size="small" onClick={() => setChangePassword(true)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.change-password" defaultMessage="Change password" />
|
||||
</div>
|
||||
<FormattedMessage id="button.change-password" defaultMessage="Change password" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<dl className={styles.list}>
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTransition, animated } from 'react-spring';
|
||||
import Button from 'components/common/Button';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import Sun from 'assets/sun.svg';
|
||||
import Moon from 'assets/moon.svg';
|
||||
@ -27,7 +26,7 @@ export default function ThemeButton() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Button className={styles.button} variant="light" onClick={handleClick}>
|
||||
<div className={styles.button} onClick={handleClick}>
|
||||
{transitions.map(({ item, key, props }) =>
|
||||
item === 'light' ? (
|
||||
<animated.div key={key} style={props}>
|
||||
@ -39,6 +38,6 @@ export default function ThemeButton() {
|
||||
</animated.div>
|
||||
),
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,5 +1,10 @@
|
||||
.button {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button svg {
|
||||
|
@ -52,14 +52,10 @@ export default function WebsiteSettings() {
|
||||
onClick={() => setShowCode(row)}
|
||||
/>
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
</div>
|
||||
<FormattedMessage id="button.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
</div>
|
||||
<FormattedMessage id="button.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
</ButtonLayout>
|
||||
);
|
||||
@ -117,9 +113,7 @@ export default function WebsiteSettings() {
|
||||
}
|
||||
>
|
||||
<Button icon={<Plus />} size="medium" onClick={() => setAddWebsite(true)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
</div>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
</Button>
|
||||
</EmptyPlaceholder>
|
||||
);
|
||||
@ -131,9 +125,7 @@ export default function WebsiteSettings() {
|
||||
<FormattedMessage id="label.websites" defaultMessage="Websites" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddWebsite(true)}>
|
||||
<div>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
</div>
|
||||
<FormattedMessage id="button.add-website" defaultMessage="Add website" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} empty={empty} />
|
||||
|
32
hooks/usePageQuery.js
Normal file
32
hooks/usePageQuery.js
Normal file
@ -0,0 +1,32 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { getQueryString } from '../lib/url';
|
||||
|
||||
export default function usePageQuery() {
|
||||
const router = useRouter();
|
||||
const { pathname, search } = location;
|
||||
|
||||
const query = useMemo(() => {
|
||||
if (!search) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const params = search.substring(1).split('&');
|
||||
|
||||
return params.reduce((obj, item) => {
|
||||
const [key, value] = item.split('=');
|
||||
|
||||
obj[key] = decodeURIComponent(value);
|
||||
|
||||
return obj;
|
||||
}, {});
|
||||
}, [search]);
|
||||
|
||||
function resolve(params) {
|
||||
const search = getQueryString({ ...query, ...params });
|
||||
|
||||
return `${pathname}${search}`;
|
||||
}
|
||||
|
||||
return { pathname, query, resolve, router };
|
||||
}
|
@ -13,7 +13,7 @@ export const urlFilter = (data, { raw }) => {
|
||||
|
||||
const cleanUrl = url => {
|
||||
try {
|
||||
const { pathname, search } = new URL(url);
|
||||
const { pathname, search } = new URL(url, location.origin);
|
||||
|
||||
if (search.startsWith('?/')) {
|
||||
return `${pathname}${search}`;
|
||||
@ -30,7 +30,7 @@ export const urlFilter = (data, { raw }) => {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const url = cleanUrl(`http://x${x}`);
|
||||
const url = cleanUrl(x);
|
||||
|
||||
if (url) {
|
||||
if (!obj[url]) {
|
||||
|
@ -21,7 +21,7 @@ export async function runQuery(query) {
|
||||
});
|
||||
}
|
||||
|
||||
export async function rawQuery(query, ...params) {
|
||||
export async function rawQuery(query, params) {
|
||||
const db = getDatabase();
|
||||
|
||||
if (db !== POSTGRESQL && db !== MYSQL) {
|
||||
@ -285,7 +285,15 @@ export async function createAccount(data) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getMetrics(website_id, start_at, end_at) {
|
||||
export function getMetrics(website_id, start_at, end_at, url) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
let urlFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select sum(t.c) as "pageviews",
|
||||
@ -300,12 +308,11 @@ export function getMetrics(website_id, start_at, end_at) {
|
||||
from pageview
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
group by 1, 2
|
||||
) t
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
@ -316,7 +323,16 @@ export function getPageviews(
|
||||
timezone = 'utc',
|
||||
unit = 'day',
|
||||
count = '*',
|
||||
url,
|
||||
) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
let urlFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select ${getDateQuery('created_at', unit, timezone)} t,
|
||||
@ -324,16 +340,23 @@ export function getPageviews(
|
||||
from pageview
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
group by 1
|
||||
order by 1
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
export function getSessionMetrics(website_id, start_at, end_at, field) {
|
||||
export function getSessionMetrics(website_id, start_at, end_at, field, url) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
let urlFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select ${field} x, count(*) y
|
||||
@ -343,18 +366,29 @@ export function getSessionMetrics(website_id, start_at, end_at, field) {
|
||||
from pageview
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
)
|
||||
group by 1
|
||||
order by 2 desc
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
export function getPageviewMetrics(website_id, start_at, end_at, field, table, domain) {
|
||||
const filter = domain ? `and ${field} not like '%${domain}%'` : '';
|
||||
export function getPageviewMetrics(website_id, start_at, end_at, field, table, domain, url) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
let domainFilter = '';
|
||||
let urlFilter = '';
|
||||
|
||||
if (domain) {
|
||||
domainFilter = `and referrer not like $${params.length + 1}`;
|
||||
params.push(`%${domain}%`);
|
||||
}
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@ -362,18 +396,18 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, d
|
||||
from ${table}
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${filter}
|
||||
${domainFilter}
|
||||
${urlFilter}
|
||||
group by 1
|
||||
order by 2 desc
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
export function getActiveVisitors(website_id) {
|
||||
const date = subMinutes(new Date(), 5);
|
||||
const params = [website_id, date];
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
@ -382,12 +416,19 @@ export function getActiveVisitors(website_id) {
|
||||
where website_id=$1
|
||||
and created_at >= $2
|
||||
`,
|
||||
website_id,
|
||||
date,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day') {
|
||||
export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day', url) {
|
||||
const params = [website_id, start_at, end_at];
|
||||
let urlFilter = '';
|
||||
|
||||
if (url) {
|
||||
urlFilter = `and url=$${params.length + 1}`;
|
||||
params.push(decodeURIComponent(url));
|
||||
}
|
||||
|
||||
return rawQuery(
|
||||
`
|
||||
select
|
||||
@ -397,11 +438,10 @@ export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit =
|
||||
from event
|
||||
where website_id=$1
|
||||
and created_at between $2 and $3
|
||||
${urlFilter}
|
||||
group by 1, 2
|
||||
order by 2
|
||||
`,
|
||||
website_id,
|
||||
start_at,
|
||||
end_at,
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
15
lib/url.js
15
lib/url.js
@ -13,3 +13,18 @@ export function getDomainName(str) {
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
export function getQueryString(params) {
|
||||
const map = Object.keys(params).reduce((arr, key) => {
|
||||
if (params[key] !== undefined) {
|
||||
return arr.concat(`${key}=${encodeURIComponent(params[key])}`);
|
||||
}
|
||||
return arr;
|
||||
}, []);
|
||||
|
||||
if (map.length) {
|
||||
return `?${map.join('&')}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
16
lib/web.js
16
lib/web.js
@ -1,3 +1,5 @@
|
||||
import { getQueryString } from './url';
|
||||
|
||||
export const apiRequest = (method, url, body) =>
|
||||
fetch(url, {
|
||||
method,
|
||||
@ -20,19 +22,9 @@ export const apiRequest = (method, url, body) =>
|
||||
return null;
|
||||
});
|
||||
|
||||
const parseQuery = (url, params = {}) => {
|
||||
const query = Object.keys(params).reduce((values, key) => {
|
||||
if (params[key] !== undefined) {
|
||||
return values.concat(`${key}=${encodeURIComponent(params[key])}`);
|
||||
}
|
||||
return values;
|
||||
}, []);
|
||||
return query.length ? `${url}?${query.join('&')}` : url;
|
||||
};
|
||||
export const get = (url, params) => apiRequest('get', `${url}${getQueryString(params)}`);
|
||||
|
||||
export const get = (url, params) => apiRequest('get', parseQuery(url, params));
|
||||
|
||||
export const del = (url, params) => apiRequest('delete', parseQuery(url, params));
|
||||
export const del = (url, params) => apiRequest('delete', `${url}${getQueryString(params)}`);
|
||||
|
||||
export const post = (url, params) => apiRequest('post', url, JSON.stringify(params));
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "0.52.0",
|
||||
"version": "0.53.0",
|
||||
"description": "A simple, fast, website analytics alternative to Google Analytics. ",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
|
@ -11,7 +11,7 @@ export default async (req, res) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, start_at, end_at, unit, tz } = req.query;
|
||||
const { id, start_at, end_at, unit, tz, url } = req.query;
|
||||
|
||||
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
||||
return badRequest(res);
|
||||
@ -21,7 +21,7 @@ export default async (req, res) => {
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
const events = await getEvents(websiteId, startDate, endDate, tz, unit);
|
||||
const events = await getEvents(websiteId, startDate, endDate, tz, unit, url);
|
||||
|
||||
return ok(res, events);
|
||||
}
|
||||
|
@ -8,13 +8,13 @@ export default async (req, res) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, start_at, end_at } = req.query;
|
||||
const { id, start_at, end_at, url } = req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
const metrics = await getMetrics(websiteId, startDate, endDate);
|
||||
const metrics = await getMetrics(websiteId, startDate, endDate, url);
|
||||
|
||||
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
||||
obj[key] = Number(metrics[0][key]) || 0;
|
||||
|
@ -11,7 +11,7 @@ export default async (req, res) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, start_at, end_at, unit, tz } = req.query;
|
||||
const { id, start_at, end_at, unit, tz, url } = req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
@ -22,8 +22,8 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
const [pageviews, uniques] = await Promise.all([
|
||||
getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
|
||||
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),
|
||||
getPageviews(websiteId, startDate, endDate, tz, unit, '*', url),
|
||||
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id', url),
|
||||
]);
|
||||
|
||||
return ok(res, { pageviews, uniques });
|
||||
|
@ -31,7 +31,7 @@ export default async (req, res) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, type, start_at, end_at, domain } = req.query;
|
||||
const { id, type, start_at, end_at, domain, url } = req.query;
|
||||
|
||||
if (domain && !DOMAIN_REGEX.test(domain)) {
|
||||
return badRequest(res);
|
||||
@ -42,7 +42,7 @@ export default async (req, res) => {
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
if (sessionColumns.includes(type)) {
|
||||
const data = await getSessionMetrics(websiteId, startDate, endDate, type);
|
||||
const data = await getSessionMetrics(websiteId, startDate, endDate, type, url);
|
||||
|
||||
return ok(res, data);
|
||||
}
|
||||
@ -55,6 +55,7 @@ export default async (req, res) => {
|
||||
getColumn(type),
|
||||
getTable(type),
|
||||
domain,
|
||||
url,
|
||||
);
|
||||
|
||||
return ok(res, data);
|
||||
|
Loading…
Reference in New Issue
Block a user