From 4ab71c42a6a36c4a455a54236e56e7b7fbb5843c Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 15 Sep 2020 21:25:51 -0700 Subject: [PATCH 01/15] Update display of combined referrer urls. --- components/metrics/ReferrersTable.js | 6 +++--- lib/filters.js | 11 ++++++++--- lib/lang.js | 2 +- lib/url.js | 4 ++++ package.json | 2 +- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 85834212..0527c68a 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -20,9 +20,9 @@ export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpa { label: , value: FILTER_RAW }, ]; - const renderLink = ({ x: url }) => { - return url.startsWith('http') ? ( - + const renderLink = ({ w: href, x: url }) => { + return (href || url).startsWith('http') ? ( + {decodeURI(url)} ) : ( diff --git a/lib/filters.js b/lib/filters.js index e4d9e48c..ee0a1759 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -1,6 +1,6 @@ import firstBy from 'thenby'; import { BROWSERS, ISO_COUNTRIES } from './constants'; -import { removeTrailingSlash, getDomainName } from './url'; +import { removeTrailingSlash, removeWWW, getDomainName } from './url'; export const urlFilter = (data, { raw }) => { const isValidUrl = url => { @@ -55,6 +55,7 @@ export const urlFilter = (data, { raw }) => { export const refFilter = (data, { domain, domainOnly, raw }) => { const domainName = getDomainName(domain); const regex = new RegExp(`http[s]?://${domainName}`); + const links = {}; const isValidRef = ref => { return ref !== '' && !ref.startsWith('/') && !ref.startsWith('#'); @@ -85,7 +86,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { const ref = searchParams.get('ref'); const query = ref ? `?ref=${ref}` : ''; - return `${origin}${path}${query}`; + return removeTrailingSlash(`${removeWWW(hostname)}${path}`) + query; } return null; @@ -101,6 +102,10 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { const url = cleanUrl(x); + if (!domainOnly && !raw) { + links[url] = x; + } + if (url) { if (!obj[url]) { obj[url] = y; @@ -113,7 +118,7 @@ export const refFilter = (data, { domain, domainOnly, raw }) => { }, {}); return Object.keys(map) - .map(key => ({ x: key, y: map[key] })) + .map(key => ({ x: key, y: map[key], w: links[key] })) .sort(firstBy('y', -1).thenBy('x')); }; diff --git a/lib/lang.js b/lib/lang.js index d45d7ce0..8568ed4f 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -38,7 +38,7 @@ export const dateLocales = { }; export const menuOptions = [ - { label: 'English', value: 'en', display: 'EN' }, + { label: 'English', value: 'en-US', display: 'EN' }, { label: '中文', value: 'zh-CN', display: 'CN' }, { label: 'Deutsch', value: 'de-DE', display: 'DE' }, { label: 'Español', value: 'es-MX', display: 'ES' }, diff --git a/lib/url.js b/lib/url.js index 0eb4a04a..d90c390e 100644 --- a/lib/url.js +++ b/lib/url.js @@ -2,6 +2,10 @@ export function removeTrailingSlash(url) { return url && url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url; } +export function removeWWW(url) { + return url && url.length > 1 && url.startsWith('www.') ? url.slice(4) : url; +} + export function getDomainName(str) { try { return new URL(str).hostname; diff --git a/package.json b/package.json index 635fe153..65726297 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umami", - "version": "0.35.0", + "version": "0.36.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", From 10e992083aaf988b0e452ee979b24860133dd184 Mon Sep 17 00:00:00 2001 From: ym-project Date: Wed, 16 Sep 2020 20:22:16 +0800 Subject: [PATCH 02/15] Update russian - set uppercase first character in "powered-by" - localize "custom-range" --- lang/ru-RU.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lang/ru-RU.json b/lang/ru-RU.json index 5c6e9df7..c2c24a10 100644 --- a/lang/ru-RU.json +++ b/lang/ru-RU.json @@ -13,13 +13,13 @@ "button.save": "Сохранить", "button.view-details": "Посмотреть детали", "button.websites": "Сайты", - "footer.powered-by": "на движке", + "footer.powered-by": "На движке", "header.nav.dashboard": "Информационная панель", "header.nav.settings": "Настройки", "label.administrator": "Администратор", "label.confirm-password": "Подтвердить пароль", "label.current-password": "Текущий пароль", - "label.custom-range": "Custom range", + "label.custom-range": "Другой период", "label.domain": "Домен", "label.enable-share-url": "Разрешить делиться ссылкой", "label.invalid": "Некорректный", From 543a66579e606a3ac9a8448b365938626126fc21 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 16 Sep 2020 09:25:14 -0700 Subject: [PATCH 03/15] Fix delete account issue. --- pages/api/account/[id].js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/api/account/[id].js b/pages/api/account/[id].js index c87f948b..6f9beac1 100644 --- a/pages/api/account/[id].js +++ b/pages/api/account/[id].js @@ -9,7 +9,7 @@ export default async (req, res) => { const { id } = req.query; const user_id = +id; - if (is_admin) { + if (!is_admin) { return unauthorized(res); } From 81789d67235dfd40d9e4054acd966b4bd2be79f6 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 16 Sep 2020 13:13:50 -0700 Subject: [PATCH 04/15] Fix API check. --- pages/api/account/[id].js | 2 +- pages/api/accounts.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pages/api/account/[id].js b/pages/api/account/[id].js index c87f948b..6f9beac1 100644 --- a/pages/api/account/[id].js +++ b/pages/api/account/[id].js @@ -9,7 +9,7 @@ export default async (req, res) => { const { id } = req.query; const user_id = +id; - if (is_admin) { + if (!is_admin) { return unauthorized(res); } diff --git a/pages/api/accounts.js b/pages/api/accounts.js index c5a41dff..3d651601 100644 --- a/pages/api/accounts.js +++ b/pages/api/accounts.js @@ -5,16 +5,16 @@ import { ok, unauthorized, methodNotAllowed } from 'lib/response'; export default async (req, res) => { await useAuth(req, res); - const { is_admin: current_user_is_admin } = req.auth; + const { is_admin } = req.auth; + + if (!is_admin) { + return unauthorized(res); + } if (req.method === 'GET') { - if (current_user_is_admin) { - const accounts = await getAccounts(); + const accounts = await getAccounts(); - return ok(res, accounts); - } - - return unauthorized(res); + return ok(res, accounts); } return methodNotAllowed(res); From 60b17363e1066d0ff5ec6ca0081a350974befac0 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 16 Sep 2020 16:28:54 -0700 Subject: [PATCH 05/15] Added date picker filter. --- components/common/DateFilter.js | 5 ++- components/forms/DatePickerForm.js | 52 +++++++++++++++++++--- components/forms/DatePickerForm.module.css | 25 ++++++++--- lib/date.js | 2 +- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index c43cb860..fb76a081 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -1,6 +1,6 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { endOfYear } from 'date-fns'; +import { endOfYear, isSameDay } from 'date-fns'; import Modal from './Modal'; import DropDown from './DropDown'; import DatePickerForm from 'components/forms/DatePickerForm'; @@ -112,7 +112,8 @@ const CustomRange = ({ startDate, endDate, onClick }) => { return ( <> } className="mr-2" onClick={handleClick} /> - {`${dateFormat(startDate, 'd LLL y', locale)} — ${dateFormat(endDate, 'd LLL y', locale)}`} + {dateFormat(startDate, 'd LLL y', locale)} + {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} ); }; diff --git a/components/forms/DatePickerForm.js b/components/forms/DatePickerForm.js index 8ead373d..f8b6416e 100644 --- a/components/forms/DatePickerForm.js +++ b/components/forms/DatePickerForm.js @@ -1,11 +1,15 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { isAfter } from 'date-fns'; +import { isAfter, isBefore, isSameDay } from 'date-fns'; import Calendar from 'components/common/Calendar'; import Button from 'components/common/Button'; import { FormButtons } from 'components/layout/FormLayout'; import { getDateRangeValues } from 'lib/date'; import styles from './DatePickerForm.module.css'; +import ButtonGroup from '../common/ButtonGroup'; + +const FILTER_DAY = 0; +const FILTER_RANGE = 1; export default function DatePickerForm({ startDate: defaultStartDate, @@ -15,21 +19,59 @@ export default function DatePickerForm({ onChange, onClose, }) { + const [selected, setSelected] = useState( + isSameDay(defaultStartDate, defaultEndDate) ? FILTER_DAY : FILTER_RANGE, + ); + const [date, setDate] = useState(defaultStartDate); const [startDate, setStartDate] = useState(defaultStartDate); const [endDate, setEndDate] = useState(defaultEndDate); + const disabled = + selected === FILTER_DAY + ? isAfter(minDate, date) && isBefore(maxDate, date) + : isAfter(startDate, endDate); + + const buttons = [ + { + label: , + value: FILTER_DAY, + }, + { + label: , + value: FILTER_RANGE, + }, + ]; + function handleSave() { - onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' }); + if (selected === FILTER_DAY) { + onChange({ ...getDateRangeValues(date, date), value: 'custom' }); + } else { + onChange({ ...getDateRangeValues(startDate, endDate), value: 'custom' }); + } } return (
+
+ +
- - + {selected === FILTER_DAY ? ( + + ) : ( + <> + + + + )}
-
- {!selectMonth && !selectYear && ( - - )} - {selectMonth && ( - - )} - {selectYear && ( - - )} +
+ {!selectMonth && !selectYear && ( + + )} + {selectMonth && ( + + )} + {selectYear && ( + + )} +
); } @@ -220,42 +222,46 @@ const YearSelector = ({ date, minDate, maxDate, onSelect }) => { return (
-
+
+ + + {chunk(years, 5).map((row, i) => ( + + {row.map((n, j) => ( + + ))} + + ))} + +
maxYear, + })} + onClick={() => (n < minYear || n > maxYear ? null : handleSelect(n))} + > + {n} +
+
+
+
); }; diff --git a/components/common/Calendar.module.css b/components/common/Calendar.module.css index b7e56df1..eb07431b 100644 --- a/components/common/Calendar.module.css +++ b/components/common/Calendar.module.css @@ -3,11 +3,11 @@ flex-direction: column; font-size: var(--font-size-small); flex: 1; - min-height: 285px; + min-height: 306px; } .calendar table { - flex: 1; + width: 100%; border-spacing: 5px; } @@ -64,18 +64,34 @@ font-size: var(--font-size-normal); } +.body { + display: flex; +} + .selector { cursor: pointer; } .pager { display: flex; + flex: 1; } .pager button { align-self: center; } +.middle { + flex: 1; +} + +.left, +.right { + display: flex; + justify-content: center; + align-items: center; +} + .left svg { transform: rotate(90deg); } From 53c23a280b09b6a071a256b41c8a01d8e54a3742 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 16 Sep 2020 21:55:32 -0700 Subject: [PATCH 07/15] Added router navigation for settings and details. --- components/WebsiteDetails.js | 67 +++++++++++------------- components/WebsiteDetails.module.css | 6 +-- components/common/NavMenu.js | 32 +++++++++++ components/common/NavMenu.module.css | 20 +++++++ components/common/UserButton.js | 3 ++ components/layout/MenuLayout.js | 26 +++++---- components/layout/MenuLayout.module.css | 19 ++----- components/settings/Settings.js | 20 ++++--- lang/de-DE.json | 3 ++ lang/en-US.json | 3 ++ lang/es-MX.json | 3 ++ lang/fr-FR.json | 3 ++ lang/ja-JP.json | 3 ++ lang/mn-MN.json | 3 ++ lang/nl-NL.json | 3 ++ lang/ru-RU.json | 3 ++ lang/tr-TR.json | 3 ++ lang/zh-CN.json | 3 ++ pages/settings/accounts.js | 3 ++ pages/{settings.js => settings/index.js} | 0 pages/settings/profile.js | 3 ++ 21 files changed, 156 insertions(+), 73 deletions(-) create mode 100644 components/common/NavMenu.js create mode 100644 components/common/NavMenu.module.css create mode 100644 pages/settings/accounts.js rename pages/{settings.js => settings/index.js} (100%) create mode 100644 pages/settings/profile.js diff --git a/components/WebsiteDetails.js b/components/WebsiteDetails.js index 3a933638..4604227e 100644 --- a/components/WebsiteDetails.js +++ b/components/WebsiteDetails.js @@ -1,5 +1,6 @@ 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,12 +20,28 @@ import EventsChart from './metrics/EventsChart'; import useFetch from 'hooks/useFetch'; import Loading from 'components/common/Loading'; +const views = { + url: PagesTable, + referrer: ReferrersTable, + browser: BrowsersTable, + os: OSTable, + device: DevicesTable, + country: CountriesTable, + event: EventsTable, +}; + export default function WebsiteDetails({ websiteId }) { + const router = useRouter(); const { data } = useFetch(`/api/website/${websiteId}`); const [chartLoaded, setChartLoaded] = useState(false); const [countryData, setCountryData] = useState(); const [eventsData, setEventsData] = useState(); - const [expand, setExpand] = useState(); + const { + query: { id, view }, + } = router; + const path = `/website/${id.join('/')}`; + + console.log({ router }); const BackButton = () => ( diff --git a/components/layout/Footer.js b/components/layout/Footer.js index 41810f26..7bd1ebd3 100644 --- a/components/layout/Footer.js +++ b/components/layout/Footer.js @@ -11,7 +11,7 @@ export default function Footer() {
- + - + diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index 83ae00b8..3335bd74 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -20,7 +20,7 @@ export default function ActiveUsers({ websiteId, className }) {
diff --git a/components/settings/AccountSettings.js b/components/settings/AccountSettings.js index 21ca04f5..bc555a0f 100644 --- a/components/settings/AccountSettings.js +++ b/components/settings/AccountSettings.js @@ -92,7 +92,7 @@ export default function AccountSettings() { <>
- +
- +
)} {view && ( - + )} diff --git a/components/WebsiteDetails.module.css b/components/WebsiteDetails.module.css index cbde6159..4f117ba1 100644 --- a/components/WebsiteDetails.module.css +++ b/components/WebsiteDetails.module.css @@ -10,6 +10,10 @@ font-size: var(--font-size-small); } +.content { + min-height: 600px; +} + .backButton { align-self: flex-start; margin-bottom: 16px; diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js index b3e00a27..1fa02f48 100644 --- a/components/common/RefreshButton.js +++ b/components/common/RefreshButton.js @@ -5,7 +5,7 @@ import { setDateRange } from 'redux/actions/websites'; import Button from './Button'; import Refresh from 'assets/redo.svg'; import Dots from 'assets/ellipsis-h.svg'; -import { useDateRange } from 'hooks/useDateRange'; +import useDateRange from 'hooks/useDateRange'; import { getDateRange } from '../../lib/date'; export default function RefreshButton({ websiteId }) { diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index 3335bd74..3d7b7001 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -4,8 +4,8 @@ import useFetch from 'hooks/useFetch'; import styles from './ActiveUsers.module.css'; import { FormattedMessage } from 'react-intl'; -export default function ActiveUsers({ websiteId, className }) { - const { data } = useFetch(`/api/website/${websiteId}/active`, {}, { interval: 60000 }); +export default function ActiveUsers({ websiteId, token, className }) { + const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 }); const count = useMemo(() => { return data?.[0]?.x || 0; }, [data]); diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index 36cfe82f..f092e62f 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import { browserFilter } from 'lib/filters'; -export default function BrowsersTable({ websiteId, limit, onExpand }) { +export default function BrowsersTable({ websiteId, token, limit, onExpand }) { return ( } type="browser" metric={} websiteId={websiteId} + token={token} limit={limit} dataFilter={browserFilter} onExpand={onExpand} diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index e7857b6d..1f516653 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -3,13 +3,20 @@ import MetricsTable from './MetricsTable'; import { countryFilter, percentFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; -export default function CountriesTable({ websiteId, limit, onDataLoad = () => {}, onExpand }) { +export default function CountriesTable({ + websiteId, + token, + limit, + onDataLoad = () => {}, + onExpand, +}) { return ( } type="country" metric={} websiteId={websiteId} + token={token} limit={limit} dataFilter={countryFilter} onDataLoad={data => onDataLoad(percentFilter(data))} diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index c37d18ea..85d2bdfd 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -4,13 +4,14 @@ import { deviceFilter } from 'lib/filters'; import { FormattedMessage } from 'react-intl'; import { getDeviceMessage } from 'components/messages'; -export default function DevicesTable({ websiteId, limit, onExpand }) { +export default function DevicesTable({ websiteId, token, limit, onExpand }) { return ( } type="device" metric={} websiteId={websiteId} + token={token} limit={limit} dataFilter={deviceFilter} renderLabel={({ x }) => getDeviceMessage(x)} diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index 1d8bb557..4a49d129 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -3,7 +3,7 @@ import tinycolor from 'tinycolor2'; import BarChart from './BarChart'; import { getTimezone, getDateArray, getDateLength } from 'lib/date'; import useFetch from 'hooks/useFetch'; -import { useDateRange } from 'hooks/useDateRange'; +import useDateRange from 'hooks/useDateRange'; const COLORS = [ '#2680eb', @@ -16,7 +16,7 @@ const COLORS = [ '#85d044', ]; -export default function EventsChart({ websiteId }) { +export default function EventsChart({ websiteId, token }) { const dateRange = useDateRange(websiteId); const { startDate, endDate, unit, modified } = dateRange; const { data } = useFetch( @@ -26,6 +26,7 @@ export default function EventsChart({ websiteId }) { end_at: +endDate, unit, tz: getTimezone(), + token, }, { update: [modified] }, ); diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js index ccc13411..948b9f7a 100644 --- a/components/metrics/EventsTable.js +++ b/components/metrics/EventsTable.js @@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; import styles from './EventsTable.module.css'; -export default function EventsTable({ websiteId, limit, onExpand, onDataLoad }) { +export default function EventsTable({ websiteId, token, limit, onExpand, onDataLoad }) { return ( } type="event" metric={} websiteId={websiteId} + token={token} limit={limit} renderLabel={({ x }) =>