diff --git a/components/common/FilterButtons.js b/components/common/FilterButtons.js new file mode 100644 index 00000000..5b898bf4 --- /dev/null +++ b/components/common/FilterButtons.js @@ -0,0 +1,11 @@ +import React from 'react'; +import ButtonLayout from 'components/layout/ButtonLayout'; +import ButtonGroup from './ButtonGroup'; + +export default function FilterButtons({ buttons, selected, onClick }) { + return ( + + + + ); +} diff --git a/components/metrics/DataTable.js b/components/metrics/DataTable.js index 70eae89f..5cf9ddb4 100644 --- a/components/metrics/DataTable.js +++ b/components/metrics/DataTable.js @@ -12,7 +12,7 @@ export default function DataTable({ metric, className, renderLabel, - height = 400, + height, animate = true, virtualize = false, }) { @@ -49,7 +49,7 @@ export default function DataTable({ {metric} -
+
{data?.length === 0 && } {virtualize && data.length > 0 ? ( diff --git a/components/metrics/DataTable.module.css b/components/metrics/DataTable.module.css index a1dc61fc..79a60577 100644 --- a/components/metrics/DataTable.module.css +++ b/components/metrics/DataTable.module.css @@ -8,7 +8,7 @@ .body { position: relative; - flex: 1; + overflow: auto; } .header { diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 5d96ed41..59e9bf62 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -17,16 +17,11 @@ import styles from './MetricsTable.module.css'; export default function MetricsTable({ websiteId, websiteDomain, - title, - metric, type, className, dataFilter, filterOptions, limit, - virtualize, - renderLabel, - height, onDataLoad, ...props }) { @@ -71,20 +66,9 @@ export default function MetricsTable({
{!data && loading && } {error && } - {data && !error && ( - - )} + {data && !error && }
- {limit && ( + {data && !error && limit && ( } href={router.pathname} diff --git a/components/metrics/PagesTable.js b/components/metrics/PagesTable.js index 978dd22c..c137589f 100644 --- a/components/metrics/PagesTable.js +++ b/components/metrics/PagesTable.js @@ -2,14 +2,15 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import Link from 'next/link'; -import ButtonGroup from 'components/common/ButtonGroup'; -import ButtonLayout from 'components/layout/ButtonLayout'; +import FilterButtons from 'components/common/FilterButtons'; import { urlFilter } from 'lib/filters'; -import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants'; import usePageQuery from 'hooks/usePageQuery'; import MetricsTable from './MetricsTable'; import styles from './PagesTable.module.css'; +export const FILTER_COMBINED = 0; +export const FILTER_RAW = 1; + export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) { const [filter, setFilter] = useState(FILTER_COMBINED); const { @@ -56,11 +57,3 @@ export default function PagesTable({ websiteId, websiteDomain, showFilters, ...p ); } - -const FilterButtons = ({ buttons, selected, onClick }) => { - return ( - - - - ); -}; diff --git a/components/metrics/RealtimeChart.js b/components/metrics/RealtimeChart.js index cd0bd788..6acfd653 100644 --- a/components/metrics/RealtimeChart.js +++ b/components/metrics/RealtimeChart.js @@ -2,7 +2,7 @@ import React, { useMemo, useRef } from 'react'; import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns'; import PageviewsChart from './PageviewsChart'; import { getDateArray } from 'lib/date'; -import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; +import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants'; function mapData(data) { let last = 0; @@ -26,7 +26,7 @@ function mapData(data) { export default function RealtimeChart({ data, unit, ...props }) { const endDate = startOfMinute(new Date()); - const startDate = subMinutes(endDate, 30); + const startDate = subMinutes(endDate, REALTIME_RANGE); const prevEndDate = useRef(endDate); const chartData = useMemo(() => { @@ -51,7 +51,7 @@ export default function RealtimeChart({ data, unit, ...props }) { return ( , @@ -28,11 +30,16 @@ const TYPE_ICONS = { export default function RealtimeLog({ data, websites }) { const [locale] = useLocale(); const countryNames = useCountryNames(locale); + const [filter, setFilter] = useState(TYPE_ALL); const logs = useMemo(() => { const { pageviews, sessions, events } = data; - return [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); - }, [data]); + const logs = [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); + if (filter) { + return logs.filter(row => getType(row) === filter); + } + return logs; + }, [data, filter]); const uuids = useMemo(() => { return data.sessions.reduce((obj, { session_id, session_uuid }) => { @@ -41,6 +48,25 @@ export default function RealtimeLog({ data, websites }) { }, {}); }, [data]); + const buttons = [ + { + label: , + value: TYPE_ALL, + }, + { + label: , + value: TYPE_PAGEVIEW, + }, + { + label: , + value: TYPE_SESSION, + }, + { + label: , + value: TYPE_EVENT, + }, + ]; + function getType({ view_id, session_id, event_id }) { if (event_id) { return TYPE_EVENT; @@ -88,7 +114,12 @@ export default function RealtimeLog({ data, websites }) { {countryNames[country]}, + browser: BROWSERS[browser], + os, + device, + }} /> ); } @@ -123,6 +154,7 @@ export default function RealtimeLog({ data, websites }) { return (
+
diff --git a/components/metrics/RealtimeViews.js b/components/metrics/RealtimeViews.js new file mode 100644 index 00000000..f07facef --- /dev/null +++ b/components/metrics/RealtimeViews.js @@ -0,0 +1,100 @@ +import React, { useMemo, useState, useCallback } from 'react'; +import { FormattedMessage } from 'react-intl'; +import firstBy from 'thenby'; +import { percentFilter } from 'lib/filters'; +import DataTable from './DataTable'; +import FilterButtons from 'components/common/FilterButtons'; + +const FILTER_REFERRERS = 0; +const FILTER_PAGES = 1; + +export default function RealtimeViews({ websiteId, data, websites }) { + const { pageviews } = data; + const [filter, setFilter] = useState(FILTER_REFERRERS); + const domains = useMemo(() => websites.map(({ domain }) => domain), [websites]); + const getDomain = useCallback( + id => websites.find(({ website_id }) => website_id === id)?.domain, + [websites], + ); + + const buttons = [ + { + label: , + value: FILTER_REFERRERS, + }, + { + label: , + value: FILTER_PAGES, + }, + ]; + + const [referrers, pages] = useMemo(() => { + if (pageviews) { + const referrers = percentFilter( + pageviews + .reduce((arr, { referrer }) => { + if (referrer?.startsWith('http')) { + const hostname = new URL(referrer).hostname.replace(/^www\./, ''); + + if (hostname && !domains.includes(hostname)) { + const row = arr.find(({ x }) => x === hostname); + + if (!row) { + arr.push({ x: hostname, y: 1 }); + } else { + row.y += 1; + } + } + } + return arr; + }, []) + .sort(firstBy('y', -1)), + ); + + const pages = percentFilter( + pageviews + .reduce((arr, { url, website_id }) => { + if (url?.startsWith('/')) { + if (!websiteId) { + url = `${getDomain(website_id)}${url}`; + } + const row = arr.find(({ x }) => x === url); + + if (!row) { + arr.push({ x: url, y: 1 }); + } else { + row.y += 1; + } + } + return arr; + }, []) + .sort(firstBy('y', -1)), + ); + + return [referrers, pages]; + } + return []; + }, [pageviews]); + + return ( + <> + + {filter === FILTER_REFERRERS && ( + } + metric={} + data={referrers} + height={400} + /> + )} + {filter === FILTER_PAGES && ( + } + metric={} + data={pages} + height={400} + /> + )} + + ); +} diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 2e04b6dc..2d51ab74 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -1,11 +1,13 @@ import React, { useState } from 'react'; import { FormattedMessage } from 'react-intl'; import MetricsTable from './MetricsTable'; -import ButtonGroup from 'components/common/ButtonGroup'; -import ButtonLayout from 'components/layout/ButtonLayout'; -import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants'; +import FilterButtons from 'components/common/FilterButtons'; import { refFilter } from 'lib/filters'; +export const FILTER_DOMAIN_ONLY = 0; +export const FILTER_COMBINED = 1; +export const FILTER_RAW = 2; + export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) { const [filter, setFilter] = useState(FILTER_COMBINED); @@ -52,11 +54,3 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ); } - -const FilterButtons = ({ buttons, selected, onClick }) => { - return ( - - - - ); -}; diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 2dbf858c..1a2a3f54 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -9,16 +9,14 @@ import RealtimeLog from 'components/metrics/RealtimeLog'; import RealtimeHeader from 'components/metrics/RealtimeHeader'; import WorldMap from 'components/common/WorldMap'; import DataTable from 'components/metrics/DataTable'; +import RealtimeViews from 'components/metrics/RealtimeViews'; import useFetch from 'hooks/useFetch'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { percentFilter } from 'lib/filters'; -import { TOKEN_HEADER } from 'lib/constants'; +import { TOKEN_HEADER, REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; import styles from './RealtimeDashboard.module.css'; -const REALTIME_RANGE = 30; -const REALTIME_INTERVAL = 3000; - function mergeData(state, data, time) { const ids = state.map(({ __id }) => __id); return state @@ -43,7 +41,10 @@ export default function RealtimeDashboard() { headers: { [TOKEN_HEADER]: init?.token }, }); - const renderCountryName = useCallback(({ x }) => countryNames[x], []); + const renderCountryName = useCallback( + ({ x }) => {countryNames[x]}, + [countryNames], + ); const realtimeData = useMemo(() => { if (data) { @@ -83,38 +84,11 @@ export default function RealtimeDashboard() { return []; }, [realtimeData?.sessions]); - const referrers = useMemo(() => { - if (realtimeData?.pageviews) { - return percentFilter( - realtimeData.pageviews - .reduce((arr, { referrer }) => { - if (referrer?.startsWith('http')) { - const { hostname } = new URL(referrer); - - if (!data.domains.includes(hostname)) { - const row = arr.find(({ x }) => x === hostname); - - if (!row) { - arr.push({ x: hostname, y: 1 }); - } else { - row.y += 1; - } - } - } - return arr; - }, []) - .sort(firstBy('y', -1)), - ); - } - return []; - }, [realtimeData?.pageviews]); - useEffect(() => { if (init && !data) { const { websites, data } = init; - const domains = init.websites.map(({ domain }) => domain); - setData({ websites, domains, ...data }); + setData({ websites, ...data }); } }, [init]); @@ -158,12 +132,7 @@ export default function RealtimeDashboard() { - } - metric={} - data={referrers} - height={400} - /> + diff --git a/components/pages/WebsiteDetails.js b/components/pages/WebsiteDetails.js index b808f44f..7385bf3f 100644 --- a/components/pages/WebsiteDetails.js +++ b/components/pages/WebsiteDetails.js @@ -173,7 +173,14 @@ export default function WebsiteDetails({ websiteId }) { contentClassName={styles.content} menu={menuOptions} > - + )} diff --git a/lib/auth.js b/lib/auth.js index 379f6777..acfbe422 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -33,7 +33,7 @@ export async function allowQuery(req, skipToken) { const website = await getWebsiteById(websiteId); if (website) { - if (token && !skipToken) { + if (token && token !== 'undefined' && !skipToken) { return isValidToken(token, { website_id: websiteId }); } diff --git a/lib/constants.js b/lib/constants.js index 72f7fce6..65702620 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -6,6 +6,15 @@ export const THEME_CONFIG = 'umami.theme'; export const VERSION_CHECK = 'umami.version-check'; export const TOKEN_HEADER = 'x-umami-token'; +export const DEFAULT_LOCALE = 'en-US'; +export const DEFAULT_THEME = 'light'; +export const DEFAUL_CHART_HEIGHT = 400; +export const DEFAULT_ANIMATION_DURATION = 300; +export const DEFAULT_DATE_RANGE = '24hour'; + +export const REALTIME_RANGE = 30; +export const REALTIME_INTERVAL = 3000; + export const THEME_COLORS = { light: { primary: '#2680eb', @@ -52,12 +61,6 @@ export const EVENT_COLORS = [ '#ffec16', ]; -export const DEFAULT_LOCALE = 'en-US'; -export const DEFAULT_THEME = 'light'; -export const DEFAUL_CHART_HEIGHT = 400; -export const DEFAULT_ANIMATION_DURATION = 300; -export const DEFAULT_DATE_RANGE = '24hour'; - export const POSTGRESQL = 'postgresql'; export const MYSQL = 'mysql'; @@ -77,10 +80,6 @@ export const POSTGRESQL_DATE_FORMATS = { year: 'YYYY-01-01', }; -export const FILTER_DOMAIN_ONLY = 0; -export const FILTER_COMBINED = 1; -export const FILTER_RAW = 2; - export const DOMAIN_REGEX = /localhost(:\d{1,5})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63}/; export const DESKTOP_SCREEN_WIDTH = 1920;