diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index e3a7e2c2..0fa20284 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -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> diff --git a/components/WebsiteList.js b/components/WebsiteList.js index b1819748..0df24877 100644 --- a/components/WebsiteList.js +++ b/components/WebsiteList.js @@ -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> )} diff --git a/components/common/Button.js b/components/common/Button.js index b973b36e..f769ba6f 100644 --- a/components/common/Button.js +++ b/components/common/Button.js @@ -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> ); diff --git a/components/common/Button.module.css b/components/common/Button.module.css index 324bbb22..51341fee 100644 --- a/components/common/Button.module.css +++ b/components/common/Button.module.css @@ -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); diff --git a/components/common/Icon.module.css b/components/common/Icon.module.css index 47d0ab0d..5b431668 100644 --- a/components/common/Icon.module.css +++ b/components/common/Icon.module.css @@ -5,10 +5,6 @@ vertical-align: middle; } -.icon + * { - margin-left: 10px; -} - .icon svg { fill: currentColor; } diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index afc3e952..113c6f56 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -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] }, diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index cad4c00e..f5d888d4 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -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; diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 75fd579c..16b5db90 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -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> diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index ba04f871..46a17fdb 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -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} /> </> diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 93552cc5..3b8d607d 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -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" diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js index ecbd0798..ae0907fb 100644 --- a/components/metrics/WebsiteChart.js +++ b/components/metrics/WebsiteChart.js @@ -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> + ); +}; diff --git a/components/metrics/WebsiteChart.module.css b/components/metrics/WebsiteChart.module.css index ea0fcaee..29f94670 100644 --- a/components/metrics/WebsiteChart.module.css +++ b/components/metrics/WebsiteChart.module.css @@ -36,6 +36,11 @@ align-items: center; } +.url { + text-align: center; + margin-bottom: 10px; +} + @media only screen and (max-width: 992px) { .filter { display: block; diff --git a/components/settings/AccountSettings.js b/components/settings/AccountSettings.js index bf02f4b3..aa206fa9 100644 --- a/components/settings/AccountSettings.js +++ b/components/settings/AccountSettings.js @@ -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} /> diff --git a/components/settings/ProfileSettings.js b/components/settings/ProfileSettings.js index f28226c5..e23c73ed 100644 --- a/components/settings/ProfileSettings.js +++ b/components/settings/ProfileSettings.js @@ -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}> diff --git a/components/settings/ThemeButton.js b/components/settings/ThemeButton.js index a31440b7..6f32e23b 100644 --- a/components/settings/ThemeButton.js +++ b/components/settings/ThemeButton.js @@ -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> ); } diff --git a/components/settings/ThemeButton.module.css b/components/settings/ThemeButton.module.css index 84fa7139..bc941834 100644 --- a/components/settings/ThemeButton.module.css +++ b/components/settings/ThemeButton.module.css @@ -1,5 +1,10 @@ .button { width: 50px; + height: 50px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; } .button svg { diff --git a/components/settings/WebsiteSettings.js b/components/settings/WebsiteSettings.js index 23f6162b..17fc5952 100644 --- a/components/settings/WebsiteSettings.js +++ b/components/settings/WebsiteSettings.js @@ -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} /> diff --git a/hooks/usePageQuery.js b/hooks/usePageQuery.js new file mode 100644 index 00000000..ced19702 --- /dev/null +++ b/hooks/usePageQuery.js @@ -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 }; +} diff --git a/lib/filters.js b/lib/filters.js index acdab7a3..dcbb9907 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -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]) { diff --git a/lib/queries.js b/lib/queries.js index c16f1ac0..af2b7f5b 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -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, ); } diff --git a/lib/url.js b/lib/url.js index d90c390e..49acedf7 100644 --- a/lib/url.js +++ b/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 ''; +} diff --git a/lib/web.js b/lib/web.js index 82c1e75b..a20a09c8 100644 --- a/lib/web.js +++ b/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)); diff --git a/package.json b/package.json index 92a55bee..f0eab672 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/api/website/[id]/events.js b/pages/api/website/[id]/events.js index 4b9a656d..da610f17 100644 --- a/pages/api/website/[id]/events.js +++ b/pages/api/website/[id]/events.js @@ -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); } diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js index bb5c977b..f7178bf4 100644 --- a/pages/api/website/[id]/metrics.js +++ b/pages/api/website/[id]/metrics.js @@ -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; diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index 016c6646..2191a4c4 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -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 }); diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index 36669c4e..9db8975e 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -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);