From 67bab5ff24685d164246a7b2f1198661ff8781d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmundur=20M=C3=B8rk=C3=B8re?= Date: Thu, 24 Sep 2020 07:11:58 +0000 Subject: [PATCH 1/9] =?UTF-8?q?=F0=9F=93=A6=20NEW:=20Added=20language:=20F?= =?UTF-8?q?aroese?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lang/fo-FO.json | 95 +++++++++++++++++++++++++++++++++++++++++++++++++ lib/lang.js | 4 +++ 2 files changed, 99 insertions(+) create mode 100644 lang/fo-FO.json diff --git a/lang/fo-FO.json b/lang/fo-FO.json new file mode 100644 index 00000000..c852a001 --- /dev/null +++ b/lang/fo-FO.json @@ -0,0 +1,95 @@ +{ + "button.add-account": "Ger brúkara", + "button.add-website": "Legg heimasíðu til", + "button.back": "Aftur", + "button.cancel": "Strika", + "button.change-password": "Broyt loyniorð", + "button.copy-to-clipboard": "Kopier til clipboard", + "button.date-range": "Vel dato", + "button.delete": "Sletta", + "button.edit": "Ger broyting", + "button.login": "Rita inn", + "button.more": "Meira", + "button.refresh": "Endurskapa", + "button.reset": "Nulstilla", + "button.save": "Goym", + "button.single-day": "Einkultur dagur", + "button.view-details": "Vís upplýsingar", + "label.accounts": "Brúkarar", + "label.administrator": "Administrator", + "label.confirm-password": "Vátta loyniorð", + "label.current-password": "Núverandi loyniorð", + "label.custom-range": "Custom range", + "label.dashboard": "Yvirlitsskíggi", + "label.default-date-range": "Standard dato", + "label.domain": "Økisnavn", + "label.enable-share-url": "Aktivera deil leinki", + "label.invalid": "Ógilda", + "label.invalid-domain": "Ógilda økisnavn", + "label.last-days": "Seinastu {x} dagarnar", + "label.last-hours": "Seinastu {x} tímanar", + "label.logged-in-as": "Ritaður inn sum {username}", + "label.logout": "Rita út", + "label.name": "Navn", + "label.new-password": "Nýtt loyniorð", + "label.password": "Loyniorð", + "label.passwords-dont-match": "Loyniorðini eru ikki eins", + "label.profile": "Brúkari", + "label.required": "Krav", + "label.settings": "Instillingar", + "label.this-month": "Hendan mánan", + "label.this-week": "Hesa vikuna", + "label.this-year": "Hetta árið", + "label.timezone": "Tíðarsona", + "label.today": "Í dag", + "label.unknown": "Ókent", + "label.username": "Brúkaranavn", + "label.websites": "Heimasíður", + "message.active-users": "{x} í løtuni {x, plural, one {vitjandi} other { vitjandi }}", + "message.confirm-delete": "Ert tú sikkur at tú ynskir at sletta {target}?", + "message.copied": "Kopiera!", + "message.delete-warning": "Øll data ið er knýtt at verður eisini sletta.", + "message.failure": "Okkurt bleiv gali.", + "message.get-share-url": "Fá leinkju sum tú kanst deila", + "message.get-tracking-code": "Fá sporing kotu", + "message.go-to-settings": "Far til instillingar", + "message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.", + "message.no-data-available": "Eingin data tøk.", + "message.no-websites-configured": "Tú hevur onga heimasíðu stillaða til.", + "message.page-not-found": "Síðan bleiv ikki funnin.", + "message.powered-by": "Powered by {name}", + "message.save-success": "Goymt.", + "message.share-url": "Hetta er tann almenna leinkjan av {target}.", + "message.track-stats": "Fyri at spora hagtøl fyri {target}, koyr kotuna í {head} partin á tínari heimasíðu.", + "message.type-delete": "Skriva {delete} í feltið fyri at vátta", + "metrics.actions": "Gerðir", + "metrics.average-visit-time": "Miðal vitjurnartíð ", + "metrics.bounce-rate": "Bounce prosenttal", + "metrics.browsers": "Kagar", + "metrics.countries": "Lond", + "metrics.device.desktop": "Borðtelda", + "metrics.device.laptop": "Fartelda", + "metrics.device.mobile": "Telefon", + "metrics.device.tablet": "Teldil", + "metrics.devices": "Eindir", + "metrics.events": "Hendingar/tiltøk", + "metrics.filter.combined": "Samansett", + "metrics.filter.domain-only": "Bara økisnavn", + "metrics.filter.raw": "Óviðgjørt", + "metrics.operating-systems": "Stýrikervi", + "metrics.page-views": "Opnaðar síðir", + "metrics.pages": "Síðir", + "metrics.referrers": "Framsendingar", + "metrics.unique-visitors": "Einsýna vitjanir", + "metrics.views": "Vitjanir", + "metrics.visitors": "Vitjandi", + "title.add-account": "Ger brúkara", + "title.add-website": "Legg avtrat heimasíðu", + "title.change-password": "Skift loyniorð", + "title.delete-account": "Sletta brúkara", + "title.delete-website": "Sletta heimasíðu", + "title.edit-account": "Broyt brúkara", + "title.edit-website": "Broyt heimasíðu", + "title.share-url": "Deil leinku", + "title.tracking-code": "Spori kota" +} diff --git a/lib/lang.js b/lib/lang.js index 066df152..5e283ee4 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -13,6 +13,7 @@ import mnMNMessages from 'lang-compiled/mn-MN.json'; import daMessages from 'lang-compiled/da-DK.json'; import svMessages from 'lang-compiled/sv-SE.json'; import grMessages from 'lang-compiled/el-GR.json'; +import foMessages from 'lang-compiled/fo-FO.json'; export const messages = { 'en-US': enMessages, @@ -28,6 +29,7 @@ export const messages = { 'da-DK': daMessages, 'sv-SE': svMessages, 'el-GR': grMessages, + 'fo-FO': foMessages, }; export const dateLocales = { @@ -44,6 +46,7 @@ export const dateLocales = { 'fr-FR': fr, 'mn-MN': enUS, 'el-GR': el, + 'fo-FO': da, }; export const menuOptions = [ @@ -60,6 +63,7 @@ export const menuOptions = [ { label: 'Русский', value: 'ru-RU', display: 'ru' }, { label: 'Svenska', value: 'sv-SE', display: 'sv' }, { label: 'Turkish', value: 'tr-TR', display: 'tr' }, + { label: 'Føroyskt', value: 'fo-FO', display: 'fø' }, ]; export function dateFormat(date, str, locale) { From 446c40ff66b7eda3f62318de3968aa73587b1fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmundur=20M=C3=B8rk=C3=B8re?= Date: Fri, 25 Sep 2020 01:15:43 +0000 Subject: [PATCH 2/9] =?UTF-8?q?=F0=9F=91=8C=20IMPROVE:=20Minor=20changes?= =?UTF-8?q?=20to=20Faroese=20translations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lang/fo-FO.json | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lang/fo-FO.json b/lang/fo-FO.json index c852a001..478dc212 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -19,13 +19,13 @@ "label.administrator": "Administrator", "label.confirm-password": "Vátta loyniorð", "label.current-password": "Núverandi loyniorð", - "label.custom-range": "Custom range", + "label.custom-range": "Tillaga spenni", "label.dashboard": "Yvirlitsskíggi", "label.default-date-range": "Standard dato", "label.domain": "Økisnavn", - "label.enable-share-url": "Aktivera deil leinki", + "label.enable-share-url": "Aktivera deili leinki", "label.invalid": "Ógilda", - "label.invalid-domain": "Ógilda økisnavn", + "label.invalid-domain": "Ógilt økisnavn", "label.last-days": "Seinastu {x} dagarnar", "label.last-hours": "Seinastu {x} tímanar", "label.logged-in-as": "Ritaður inn sum {username}", @@ -36,7 +36,7 @@ "label.passwords-dont-match": "Loyniorðini eru ikki eins", "label.profile": "Brúkari", "label.required": "Krav", - "label.settings": "Instillingar", + "label.settings": "Stillingar", "label.this-month": "Hendan mánan", "label.this-week": "Hesa vikuna", "label.this-year": "Hetta árið", @@ -50,16 +50,16 @@ "message.copied": "Kopiera!", "message.delete-warning": "Øll data ið er knýtt at verður eisini sletta.", "message.failure": "Okkurt bleiv gali.", - "message.get-share-url": "Fá leinkju sum tú kanst deila", - "message.get-tracking-code": "Fá sporing kotu", - "message.go-to-settings": "Far til instillingar", + "message.get-share-url": "Fá leinku sum tú kanst deila", + "message.get-tracking-code": "Fá sporings kotu", + "message.go-to-settings": "Far til stillingar", "message.incorrect-username-password": "Skeivt brúkaranavn/loyniorð.", - "message.no-data-available": "Eingin data tøk.", - "message.no-websites-configured": "Tú hevur onga heimasíðu stillaða til.", + "message.no-data-available": "Einki data tøk.", + "message.no-websites-configured": "Tú hevur ongar heimasíður stillaða til.", "message.page-not-found": "Síðan bleiv ikki funnin.", "message.powered-by": "Powered by {name}", "message.save-success": "Goymt.", - "message.share-url": "Hetta er tann almenna leinkjan av {target}.", + "message.share-url": "Hetta er tann almenna leinkan av {target}.", "message.track-stats": "Fyri at spora hagtøl fyri {target}, koyr kotuna í {head} partin á tínari heimasíðu.", "message.type-delete": "Skriva {delete} í feltið fyri at vátta", "metrics.actions": "Gerðir", @@ -71,12 +71,12 @@ "metrics.device.laptop": "Fartelda", "metrics.device.mobile": "Telefon", "metrics.device.tablet": "Teldil", - "metrics.devices": "Eindir", + "metrics.devices": "Tóleindir", "metrics.events": "Hendingar/tiltøk", "metrics.filter.combined": "Samansett", "metrics.filter.domain-only": "Bara økisnavn", "metrics.filter.raw": "Óviðgjørt", - "metrics.operating-systems": "Stýrikervi", + "metrics.operating-systems": "Stýrikervir", "metrics.page-views": "Opnaðar síðir", "metrics.pages": "Síðir", "metrics.referrers": "Framsendingar", @@ -84,7 +84,7 @@ "metrics.views": "Vitjanir", "metrics.visitors": "Vitjandi", "title.add-account": "Ger brúkara", - "title.add-website": "Legg avtrat heimasíðu", + "title.add-website": "Legg heimasíðu avtrat", "title.change-password": "Skift loyniorð", "title.delete-account": "Sletta brúkara", "title.delete-website": "Sletta heimasíðu", From be186483f57c6a6383fa03e0a0b1bd4ebd082c92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sigmundur=20M=C3=B8rk=C3=B8re?= Date: Fri, 25 Sep 2020 01:24:17 +0000 Subject: [PATCH 3/9] =?UTF-8?q?=F0=9F=90=9B=20FIX:=20One=20wrong=20word?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lang/fo-FO.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/fo-FO.json b/lang/fo-FO.json index 478dc212..800f91c7 100644 --- a/lang/fo-FO.json +++ b/lang/fo-FO.json @@ -23,7 +23,7 @@ "label.dashboard": "Yvirlitsskíggi", "label.default-date-range": "Standard dato", "label.domain": "Økisnavn", - "label.enable-share-url": "Aktivera deili leinki", + "label.enable-share-url": "Virkja deili leinki", "label.invalid": "Ógilda", "label.invalid-domain": "Ógilt økisnavn", "label.last-days": "Seinastu {x} dagarnar", From ee8859d86c722bbda3b875a3783c140230b77372 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 24 Sep 2020 20:20:24 -0700 Subject: [PATCH 4/9] Updated languages. --- lib/lang.js | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/lang.js b/lib/lang.js index 5e283ee4..d4415895 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -55,6 +55,7 @@ export const menuOptions = [ { label: 'Dansk', value: 'da-DK', display: 'da' }, { label: 'Deutsch', value: 'de-DE', display: 'de' }, { label: 'Español', value: 'es-MX', display: 'es' }, + { label: 'Føroyskt', value: 'fo-FO', display: 'fo' }, { label: 'Français', value: 'fr-FR', display: 'fr' }, { label: 'Ελληνικά', value: 'el-GR', display: 'el' }, { label: '日本語', value: 'ja-JP', display: 'ja' }, @@ -62,8 +63,7 @@ export const menuOptions = [ { label: 'Nederlands', value: 'nl-NL', display: 'nl' }, { label: 'Русский', value: 'ru-RU', display: 'ru' }, { label: 'Svenska', value: 'sv-SE', display: 'sv' }, - { label: 'Turkish', value: 'tr-TR', display: 'tr' }, - { label: 'Føroyskt', value: 'fo-FO', display: 'fø' }, + { label: 'Türkçe', value: 'tr-TR', display: 'tr' }, ]; export function dateFormat(date, str, locale) { diff --git a/package.json b/package.json index 639a5bc5..05766603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.50.0", + "version": "0.51.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", From 1dfa6d8b1665b357da2a1c3f8b998d8896a35c42 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 24 Sep 2020 23:02:11 -0700 Subject: [PATCH 5/9] Refactor query methods. --- lib/db.js | 6 - lib/queries.js | 291 ++++++++++++++++--------------------------------- 2 files changed, 92 insertions(+), 205 deletions(-) diff --git a/lib/db.js b/lib/db.js index e314684e..35948998 100644 --- a/lib/db.js +++ b/lib/db.js @@ -31,9 +31,3 @@ if (process.env.NODE_ENV === 'production') { } export default prisma; - -export async function runQuery(query) { - return query.catch(e => { - throw e; - }); -} diff --git a/lib/queries.js b/lib/queries.js index 684b30d4..9fd61125 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -1,5 +1,5 @@ import moment from 'moment-timezone'; -import prisma, { runQuery } from 'lib/db'; +import prisma from 'lib/db'; import { subMinutes } from 'date-fns'; import { MYSQL, POSTGRESQL, MYSQL_DATE_FORMATS, POSTGRESQL_DATE_FORMATS } from 'lib/constants'; @@ -15,7 +15,27 @@ export function getDatabase() { return type; } -export function getDateQuery(db, field, unit, timezone) { +export async function runQuery(query) { + return query.catch(e => { + throw e; + }); +} + +export async function rawQuery(query, ...params) { + const db = getDatabase(); + + if (db !== POSTGRESQL && db !== MYSQL) { + return Promise.reject(new Error('Unknown database.')); + } + + const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query; + + return prisma.$queryRaw.apply(prisma, [sql, ...params]); +} + +export function getDateQuery(field, unit, timezone) { + const db = getDatabase(); + if (db === POSTGRESQL) { if (timezone) { return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`; @@ -34,6 +54,18 @@ export function getDateQuery(db, field, unit, timezone) { } } +export function getTimestampInterval(field) { + const db = getDatabase(); + + if (db === POSTGRESQL) { + return `floor(extract(epoch from max(${field}) - min(${field})))`; + } + + if (db === MYSQL) { + return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`; + } +} + export async function getWebsiteById(website_id) { return runQuery( prisma.website.findOne({ @@ -254,61 +286,27 @@ export async function createAccount(data) { } export function getMetrics(website_id, start_at, end_at) { - const db = getDatabase(); - - if (db === POSTGRESQL) { - return runQuery( - prisma.$queryRaw( - ` + return rawQuery( + ` select sum(t.c) as "pageviews", count(distinct t.session_id) as "uniques", sum(case when t.c = 1 then 1 else 0 end) as "bounces", sum(t.time) as "totaltime" from ( select session_id, - ${getDateQuery(db, 'created_at', 'hour')}, + ${getDateQuery('created_at', 'hour')}, count(*) c, - floor(extract(epoch from max(created_at) - min(created_at))) as "time" + ${getTimestampInterval('created_at')} as "time" from pageview where website_id=$1 and created_at between $2 and $3 group by 1, 2 ) t - `, - website_id, - start_at, - end_at, - ), - ); - } - - if (db === MYSQL) { - return runQuery( - prisma.$queryRaw( - ` - select sum(t.c) as "pageviews", - count(distinct t.session_id) as "uniques", - sum(case when t.c = 1 then 1 else 0 end) as "bounces", - sum(t.time) as "totaltime" - from ( - select session_id, - ${getDateQuery(db, 'created_at', 'hour')}, - count(*) c, - floor(unix_timestamp(max(created_at)) - unix_timestamp(min(created_at))) as "time" - from pageview - where website_id=? - and created_at between ? and ? - group by 1, 2 - ) t - `, - website_id, - start_at, - end_at, - ), - ); - } - - return Promise.reject(new Error('Unknown database.')); + `, + website_id, + start_at, + end_at, + ); } export function getPageviews( @@ -319,176 +317,71 @@ export function getPageviews( unit = 'day', count = '*', ) { - const db = getDatabase(); - - if (db === POSTGRESQL) { - return runQuery( - prisma.$queryRaw( - ` - select ${getDateQuery(db, 'created_at', unit, timezone)} t, - count(${count}) y - from pageview - where website_id=$1 - and created_at between $2 and $3 - group by 1 - order by 1 - `, - website_id, - start_at, - end_at, - ), - ); - } - - if (db === MYSQL) { - return runQuery( - prisma.$queryRaw( - ` - select ${getDateQuery(db, 'created_at', unit, timezone)} t, - count(${count}) y - from pageview - where website_id=? - and created_at between ? and ? - group by 1 - order by 1 - `, - website_id, - start_at, - end_at, - ), - ); - } - - return Promise.reject(new Error('Unknown database.')); + return rawQuery( + ` + select ${getDateQuery('created_at', unit, timezone)} t, + count(${count}) y + from pageview + where website_id=$1 + and created_at between $2 and $3 + group by 1 + order by 1 + `, + website_id, + start_at, + end_at, + ); } export function getRankings(website_id, start_at, end_at, type, table, domain) { - const db = getDatabase(); - const filter = domain ? `and ${type} not like '%${domain}%'` : ''; - if (db === POSTGRESQL) { - return runQuery( - prisma.$queryRaw( - ` - select distinct ${type} x, count(*) y - from ${table} - where website_id=$1 - and created_at between $2 and $3 - ${filter} - group by 1 - order by 2 desc - `, - website_id, - start_at, - end_at, - ), - ); - } - - if (db === MYSQL) { - return runQuery( - prisma.$queryRaw( - ` - select distinct ${type} x, count(*) y - from ${table} - where website_id=? - and created_at between ? and ? - ${filter} - group by 1 - order by 2 desc - `, - website_id, - start_at, - end_at, - ), - ); - } - - return Promise.reject(new Error('Unknown database.')); + return rawQuery( + ` + select distinct ${type} x, count(*) y + from ${table} + where website_id=$1 + and created_at between $2 and $3 + ${filter} + group by 1 + order by 2 desc + `, + website_id, + start_at, + end_at, + ); } export function getActiveVisitors(website_id) { - const db = getDatabase(); const date = subMinutes(new Date(), 5); - if (db === POSTGRESQL) { - return runQuery( - prisma.$queryRaw( - ` + return rawQuery( + ` select count(distinct session_id) x from pageview where website_id=$1 and created_at >= $2 `, - website_id, - date, - ), - ); - } - - if (db === MYSQL) { - return runQuery( - prisma.$queryRaw( - ` - select count(distinct session_id) x - from pageview - where website_id=? - and created_at >= ? - `, - website_id, - date, - ), - ); - } - - return Promise.reject(new Error('Unknown database.')); + website_id, + date, + ); } export function getEvents(website_id, start_at, end_at, timezone = 'utc', unit = 'day') { - const db = getDatabase(); - - if (db === POSTGRESQL) { - return runQuery( - prisma.$queryRaw( - ` - select - event_value x, - ${getDateQuery(db, 'created_at', unit, timezone)} t, - count(*) y - from event - where website_id=$1 - and created_at between $2 and $3 - group by 1, 2 - order by 2 - `, - website_id, - start_at, - end_at, - ), - ); - } - - if (db === MYSQL) { - return runQuery( - prisma.$queryRaw( - ` - select - event_value x, - ${getDateQuery(db, 'created_at', unit, timezone)} t, - count(*) y - from event - where website_id=? - and created_at between ? and ? - group by 1, 2 - order by 2 - `, - website_id, - start_at, - end_at, - ), - ); - } - - return Promise.reject(new Error('Unknown database.')); + return rawQuery( + ` + select + event_value x, + ${getDateQuery('created_at', unit, timezone)} t, + count(*) y + from event + where website_id=$1 + and created_at between $2 and $3 + group by 1, 2 + order by 2 + `, + website_id, + start_at, + end_at, + ); } From a24bee815c6cd7d3d50bc172da9553bfb2eb00e2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Sep 2020 00:15:58 -0700 Subject: [PATCH 6/9] Update queries to calculate correct metrics. --- lib/queries.js | 26 +++++++++++++++++--- pages/api/website/[id]/rankings.js | 38 ++++++++++++++++-------------- 2 files changed, 43 insertions(+), 21 deletions(-) diff --git a/lib/queries.js b/lib/queries.js index 9fd61125..c16f1ac0 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -333,12 +333,32 @@ export function getPageviews( ); } -export function getRankings(website_id, start_at, end_at, type, table, domain) { - const filter = domain ? `and ${type} not like '%${domain}%'` : ''; +export function getSessionMetrics(website_id, start_at, end_at, field) { + return rawQuery( + ` + select ${field} x, count(*) y + from session + where session_id in ( + select session_id + from pageview + where website_id=$1 + and created_at between $2 and $3 + ) + group by 1 + order by 2 desc + `, + website_id, + start_at, + end_at, + ); +} + +export function getPageviewMetrics(website_id, start_at, end_at, field, table, domain) { + const filter = domain ? `and ${field} not like '%${domain}%'` : ''; return rawQuery( ` - select distinct ${type} x, count(*) y + select ${field} x, count(*) y from ${table} where website_id=$1 and created_at between $2 and $3 diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js index 90a0cd9f..36669c4e 100644 --- a/pages/api/website/[id]/rankings.js +++ b/pages/api/website/[id]/rankings.js @@ -1,4 +1,4 @@ -import { getRankings } from 'lib/queries'; +import { getPageviewMetrics, getSessionMetrics } from 'lib/queries'; import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; import { DOMAIN_REGEX } from 'lib/constants'; import { allowQuery } from 'lib/auth'; @@ -33,30 +33,32 @@ export default async (req, res) => { const { id, type, start_at, end_at, domain } = req.query; + if (domain && !DOMAIN_REGEX.test(domain)) { + return badRequest(res); + } + const websiteId = +id; const startDate = new Date(+start_at); const endDate = new Date(+end_at); - if ( - type !== 'event' && - !sessionColumns.includes(type) && - !pageviewColumns.includes(type) && - domain && - DOMAIN_REGEX.test(domain) - ) { - return badRequest(res); + if (sessionColumns.includes(type)) { + const data = await getSessionMetrics(websiteId, startDate, endDate, type); + + return ok(res, data); } - const rankings = await getRankings( - websiteId, - startDate, - endDate, - getColumn(type), - getTable(type), - domain, - ); + if (type === 'event' || pageviewColumns.includes(type)) { + const data = await getPageviewMetrics( + websiteId, + startDate, + endDate, + getColumn(type), + getTable(type), + domain, + ); - return ok(res, rankings); + return ok(res, data); + } } return methodNotAllowed(res); From 6bc371352c122e8c36a191f24dc4ac1f574ffa39 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Sep 2020 00:19:38 -0700 Subject: [PATCH 7/9] Bump version. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 05766603..92a55bee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.51.0", + "version": "0.52.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", From 4fded49b03dbb2a5dd1355b8702628e951238951 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 25 Sep 2020 22:31:18 -0700 Subject: [PATCH 8/9] URL filter functionality. --- components/WebsiteDetails.js | 38 +++++----- components/WebsiteList.js | 4 +- components/common/Button.js | 9 ++- components/common/Button.module.css | 16 +++- components/common/Icon.module.css | 4 - components/metrics/EventsChart.js | 3 + components/metrics/MetricsBar.js | 8 +- components/metrics/MetricsTable.js | 10 ++- components/metrics/PagesTable.js | 26 ++++++- components/metrics/ReferrersTable.js | 3 +- components/metrics/WebsiteChart.js | 24 ++++++ components/metrics/WebsiteChart.module.css | 5 ++ components/settings/AccountSettings.js | 12 +-- components/settings/ProfileSettings.js | 4 +- components/settings/ThemeButton.js | 5 +- components/settings/ThemeButton.module.css | 5 ++ components/settings/WebsiteSettings.js | 16 +--- hooks/usePageQuery.js | 32 ++++++++ lib/filters.js | 4 +- lib/queries.js | 88 ++++++++++++++++------ lib/url.js | 15 ++++ lib/web.js | 16 +--- package.json | 2 +- pages/api/website/[id]/events.js | 4 +- pages/api/website/[id]/metrics.js | 4 +- pages/api/website/[id]/pageviews.js | 6 +- pages/api/website/[id]/rankings.js | 5 +- 27 files changed, 251 insertions(+), 117 deletions(-) create mode 100644 hooks/usePageQuery.js 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 = () => ( ); @@ -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 9/9] 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 }) =>