From c6eec3ee62b8d24335ec176a7a3ee43a6a91c41e Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 22 Sep 2020 18:35:11 -0700 Subject: [PATCH 01/26] Include credentials in fetch. --- lib/web.js | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/web.js b/lib/web.js index 4081b3ab..82c1e75b 100644 --- a/lib/web.js +++ b/lib/web.js @@ -2,6 +2,7 @@ export const apiRequest = (method, url, body) => fetch(url, { method, cache: 'no-cache', + credentials: 'same-origin', headers: { Accept: 'application/json', 'Content-Type': 'application/json', diff --git a/package.json b/package.json index 543e201b..639a5bc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.49.0", + "version": "0.50.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", From 17a40d15e3975ee7ad08ba18dbbd632378984e23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20M=C3=BCller-Downing?= Date: Wed, 23 Sep 2020 21:06:50 +1000 Subject: [PATCH 02/26] Use frozen lockfile when installing dependencies with yarn --- Dockerfile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9f3070f3..b9c3a027 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,13 +8,13 @@ WORKDIR /build COPY package.json yarn.lock /build/ # Install only the production dependencies -RUN yarn install --production +RUN yarn install --frozen-lockfile # Cache these modules for production RUN cp -R node_modules/ prod_node_modules/ # Install development dependencies -RUN yarn install +RUN yarn install --frozen-lockfile COPY . /build RUN yarn build @@ -27,7 +27,7 @@ WORKDIR /app COPY --from=build /build/prod_node_modules ./node_modules # Copy generated Prisma client -COPY --from=build /build/node_modules/\.prisma/ ./node_modules/\.prisma/ +COPY --from=build /build/node_modules/.prisma/ ./node_modules/.prisma/ COPY --from=build /build/yarn.lock /build/package.json ./ COPY --from=build /build/.next ./.next From 15ea2ba913cd2c638a7b50fa81f590bc56d5d5fc Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 23 Sep 2020 08:22:40 -0700 Subject: [PATCH 03/26] Fix date issue in Safari. --- lib/date.js | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/lib/date.js b/lib/date.js index 63bad45e..cdfe322c 100644 --- a/lib/date.js +++ b/lib/date.js @@ -100,6 +100,19 @@ export function getDateRangeValues(startDate, endDate) { return { startDate: startOfDay(startDate), endDate: endOfDay(endDate), unit }; } +export function getDateFromString(str) { + const [ymd, hms] = str.split(' '); + const [year, month, day] = ymd.split('-'); + + if (hms) { + const [hour, min, sec] = hms.split(':'); + + return new Date(year, month - 1, day, hour, min, sec); + } + + return new Date(year, month - 1, day); +} + const dateFuncs = { hour: [differenceInHours, addHours, startOfHour], day: [differenceInCalendarDays, addDays, startOfDay], @@ -114,12 +127,7 @@ export function getDateArray(data, startDate, endDate, unit) { function findData(t) { const x = data.find(e => { - if (unit === 'hour') { - return normalize(new Date(e.t)).getTime() === t.getTime(); - } - - const [year, month, day] = e.t.split('-'); - return normalize(new Date(year, month - 1, day)).getTime() === t.getTime(); + return normalize(getDateFromString(e.t)).getTime() === t.getTime(); }); return x?.y || 0; From a56738597de98e7149cdc7ae5a5288724c3d5008 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 23 Sep 2020 15:41:56 -0700 Subject: [PATCH 04/26] Always show More button. Update page filter. --- components/metrics/MetricsTable.js | 2 +- lib/filters.js | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index eca58dc2..75fd579c 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -99,7 +99,7 @@ export default function MetricsTable({ )}
- {limit && data.length > limit && ( + {limit && ( ); @@ -64,31 +60,31 @@ export default function WebsiteDetails({ websiteId, token }) { }, { label: , - value: `${path}?view=url`, + value: resolve({ view: 'url' }), }, { label: , - value: `${path}?view=referrer`, + value: resolve({ view: 'referrer' }), }, { label: , - value: `${path}?view=browser`, + value: resolve({ view: 'browser' }), }, { label: , - value: `${path}?view=os`, + value: resolve({ view: 'os' }), }, { label: , - value: `${path}?view=device`, + value: resolve({ view: 'device' }), }, { label: , - value: `${path}?view=country`, + value: resolve({ view: 'country' }), }, { label: , - 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} > - + )} 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 }) { } > )} 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 && } - {children} + {icon && } + {children &&
{children}
} {tooltip && {tooltip}} ); 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({
{limit && ( )}
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: , value: FILTER_RAW }, ]; + const renderLink = ({ x }) => { + return ( + + {decodeURI(x)} + + ); + }; + return ( <> - {!limit && } + {showFilters && } } 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 && } + {showFilters && } } 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 ( <> @@ -54,6 +67,7 @@ export default function WebsiteChart({ stickyClassName={styles.sticky} enabled={stickyHeader} > + {url && }
@@ -81,3 +95,13 @@ export default function WebsiteChart({ ); } + +const PageFilter = ({ url, onClick }) => { + return ( +
+ +
+ ); +}; 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' ? ( ) : null; @@ -102,9 +98,7 @@ export default function AccountSettings() {
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() {
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 ( - + ); } 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)} /> ); @@ -117,9 +113,7 @@ export default function WebsiteSettings() { } > ); @@ -131,9 +125,7 @@ export default function WebsiteSettings() {
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 ", "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); From 35b921bdb4d966d6bc0500edfe0acc6bcc0ab77b Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Sep 2020 23:38:28 -0700 Subject: [PATCH 17/26] Convert buttons to links. --- components/WebsiteDetails.js | 22 ++++++----------- components/WebsiteDetails.module.css | 1 - components/common/Button.js | 2 +- components/common/Button.module.css | 16 ++++++------ components/common/Link.js | 15 ++++++++++-- components/common/Link.module.css | 27 +++++++++++++++++++++ components/metrics/BrowsersTable.js | 3 +-- components/metrics/CountriesTable.js | 9 +------ components/metrics/DevicesTable.js | 3 +-- components/metrics/EventsTable.js | 3 +-- components/metrics/MetricsTable.js | 16 ++++++++---- components/metrics/MetricsTable.module.css | 7 +++--- components/metrics/OSTable.js | 3 +-- components/metrics/PagesTable.js | 26 +++++++++++--------- components/metrics/PagesTable.module.css | 8 ++++++ components/metrics/ReferrersTable.js | 10 +------- components/metrics/WebsiteChart.js | 2 +- components/metrics/WebsiteHeader.js | 5 ++-- components/metrics/WebsiteHeader.module.css | 5 ---- package.json | 2 +- pages/api/website/[id]/rankings.js | 2 +- 21 files changed, 108 insertions(+), 79 deletions(-) create mode 100644 components/metrics/PagesTable.module.css diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 0fa20284..f24332ba 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -5,7 +5,8 @@ import WebsiteChart from 'components/metrics/WebsiteChart'; import WorldMap from 'components/common/WorldMap'; import Page from 'components/layout/Page'; import MenuLayout from 'components/layout/MenuLayout'; -import Button from 'components/common/Button'; +import Link from 'components/common/Link'; +import Loading from 'components/common/Loading'; import Arrow from 'assets/arrow-right.svg'; import styles from './WebsiteDetails.module.css'; import PagesTable from './metrics/PagesTable'; @@ -17,8 +18,7 @@ import CountriesTable from './metrics/CountriesTable'; 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'; +import usePageQuery from 'hooks/usePageQuery'; const views = { url: PagesTable, @@ -36,22 +36,21 @@ export default function WebsiteDetails({ websiteId, token }) { const [countryData, setCountryData] = useState(); const [eventsData, setEventsData] = useState(); const { - pathname, resolve, - router, query: { view }, } = usePageQuery(); const BackButton = () => ( - + ); const menuOptions = [ @@ -93,7 +92,6 @@ export default function WebsiteDetails({ websiteId, token }) { token, websiteDomain: data?.domain, limit: 10, - onExpand: handleExpand, }; const DetailsComponent = views[view]; @@ -104,10 +102,6 @@ export default function WebsiteDetails({ websiteId, token }) { } } - function handleExpand(value) { - router.push(resolve({ view: value })); - } - if (!data) { return null; } diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css index ca80dca0..0e1065c6 100644 --- a/components/WebsiteDetails.module.css +++ b/components/WebsiteDetails.module.css @@ -16,7 +16,6 @@ } .backButton { - align-self: flex-start; margin-bottom: 16px; } diff --git a/components/common/Button.js b/components/common/Button.js index f769ba6f..5e92d0d8 100644 --- a/components/common/Button.js +++ b/components/common/Button.js @@ -38,7 +38,7 @@ export default function Button({ {...props} > {icon && } - {children &&
{children}
} + {children &&
{children}
} {tooltip && {tooltip}} ); diff --git a/components/common/Button.module.css b/components/common/Button.module.css index 51341fee..f4fd8546 100644 --- a/components/common/Button.module.css +++ b/components/common/Button.module.css @@ -10,7 +10,6 @@ border: 0; outline: none; cursor: pointer; - white-space: nowrap; position: relative; } @@ -22,12 +21,15 @@ color: var(--gray900); } -.large { - font-size: var(--font-size-large); +.label { + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + max-width: 300px; } -.medium { - font-size: var(--font-size-normal); +.large { + font-size: var(--font-size-large); } .small { @@ -65,7 +67,7 @@ background: inherit; } -.button .icon + div { +.button .icon + * { margin-left: 10px; } @@ -74,7 +76,7 @@ margin-left: 10px; } -.button.iconRight .icon + div { +.button.iconRight .icon + * { margin: 0; } diff --git a/components/common/Link.js b/components/common/Link.js index c3a5fa7e..466e018c 100644 --- a/components/common/Link.js +++ b/components/common/Link.js @@ -1,12 +1,23 @@ import React from 'react'; import classNames from 'classnames'; import NextLink from 'next/link'; +import Icon from './Icon'; import styles from './Link.module.css'; -export default function Link({ className, children, ...props }) { +export default function Link({ className, icon, children, size, iconRight, ...props }) { return ( - {children} + + {icon && } + {children} + ); } diff --git a/components/common/Link.module.css b/components/common/Link.module.css index 24d8f84c..ea6d281d 100644 --- a/components/common/Link.module.css +++ b/components/common/Link.module.css @@ -4,6 +4,8 @@ a.link:visited { position: relative; color: var(--gray900); text-decoration: none; + display: inline-flex; + align-items: center; } a.link:before { @@ -21,3 +23,28 @@ a.link:hover:before { width: 100%; transition: width 100ms; } + +a.link.large { + font-size: var(--font-size-large); +} + +a.link.small { + font-size: var(--font-size-small); +} + +a.link.xsmall { + font-size: var(--font-size-xsmall); +} + +a.link .icon + * { + margin-left: 10px; +} + +a.link.iconRight .icon { + order: 1; + margin-left: 10px; +} + +a.link.iconRight .icon + * { + margin: 0; +} diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index f092e62f..97f9bfbd 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import { browserFilter } from 'lib/filters'; -export default function BrowsersTable({ websiteId, token, limit, onExpand }) { +export default function BrowsersTable({ websiteId, token, limit }) { return ( } @@ -13,7 +13,6 @@ export default function BrowsersTable({ websiteId, token, limit, onExpand }) { token={token} limit={limit} dataFilter={browserFilter} - onExpand={onExpand} /> ); } diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index 1f516653..58548d06 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -3,13 +3,7 @@ import MetricsTable from './MetricsTable'; import { countryFilter, percentFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; -export default function CountriesTable({ - websiteId, - token, - limit, - onDataLoad = () => {}, - onExpand, -}) { +export default function CountriesTable({ websiteId, token, limit, onDataLoad = () => {} }) { return ( } @@ -20,7 +14,6 @@ export default function CountriesTable({ limit={limit} dataFilter={countryFilter} onDataLoad={data => onDataLoad(percentFilter(data))} - onExpand={onExpand} /> ); } diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index 85d2bdfd..7d87d1c1 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -4,7 +4,7 @@ import { deviceFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; import { getDeviceMessage } from 'components/messages'; -export default function DevicesTable({ websiteId, token, limit, onExpand }) { +export default function DevicesTable({ websiteId, token, limit }) { return ( } @@ -15,7 +15,6 @@ export default function DevicesTable({ websiteId, token, limit, onExpand }) { limit={limit} dataFilter={deviceFilter} renderLabel={({ x }) => getDeviceMessage(x)} - onExpand={onExpand} /> ); } diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js index 948b9f7a..9a7a09cb 100644 --- a/components/metrics/EventsTable.js +++ b/components/metrics/EventsTable.js @@ -3,7 +3,7 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import styles from './EventsTable.module.css'; -export default function EventsTable({ websiteId, token, limit, onExpand, onDataLoad }) { +export default function EventsTable({ websiteId, token, limit, onDataLoad }) { return ( } @@ -13,7 +13,6 @@ export default function EventsTable({ websiteId, token, limit, onExpand, onDataL token={token} limit={limit} renderLabel={({ x }) =>