diff --git a/components/common/Calendar.js b/components/common/Calendar.js index 0414ff7f..237f065e 100644 --- a/components/common/Calendar.js +++ b/components/common/Calendar.js @@ -18,7 +18,7 @@ import { } from 'date-fns'; import Button from './Button'; import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/lang'; +import { dateFormat } from 'lib/date'; import { chunk } from 'lib/array'; import Chevron from 'assets/chevron-down.svg'; import Cross from 'assets/times.svg'; diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index 6279d338..45950086 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -6,8 +6,7 @@ import Modal from './Modal'; import DropDown from './DropDown'; import DatePickerForm from 'components/forms/DatePickerForm'; import useLocale from 'hooks/useLocale'; -import { getDateRange } from 'lib/date'; -import { dateFormat } from 'lib/lang'; +import { getDateRange, dateFormat } from 'lib/date'; import Calendar from 'assets/calendar-alt.svg'; import Icon from './Icon'; diff --git a/components/common/NoData.module.css b/components/common/NoData.module.css index 82f9c3ee..518fa488 100644 --- a/components/common/NoData.module.css +++ b/components/common/NoData.module.css @@ -1,8 +1,11 @@ .container { color: var(--gray500); font-size: var(--font-size-normal); - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); + position: relative; + display: flex; + align-items: center; + justify-content: center; + text-align: center; + width: 100%; + height: 100%; } diff --git a/components/layout/GridLayout.module.css b/components/layout/GridLayout.module.css index f17c195e..675fce16 100644 --- a/components/layout/GridLayout.module.css +++ b/components/layout/GridLayout.module.css @@ -35,6 +35,6 @@ .row > .col { border-top: 1px solid var(--gray300); border-left: 0; - padding: 0; + padding: 20px 0; } } diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index c3903829..856e81e8 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -3,7 +3,7 @@ import classNames from 'classnames'; import ChartJS from 'chart.js'; import Legend from 'components/metrics/Legend'; import { formatLongNumber } from 'lib/format'; -import { dateFormat, timeFormat } from 'lib/lang'; +import { dateFormat } from 'lib/date'; import useLocale from 'hooks/useLocale'; import useTheme from 'hooks/useTheme'; import { DEFAUL_CHART_HEIGHT, DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; @@ -46,7 +46,7 @@ export default function BarChart({ case 'minute': return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : ''; case 'hour': - return timeFormat(d, locale); + return dateFormat(d, 'p', locale); case 'day': if (records > 31) { if (w <= 500) { @@ -93,9 +93,9 @@ export default function BarChart({ function getTooltipFormat(unit) { switch (unit) { case 'hour': - return 'EEE ha — MMM d yyyy'; + return 'EEE p — PPP'; default: - return 'EEE MMMM d yyyy'; + return 'PPPP'; } } diff --git a/components/metrics/DataTable.module.css b/components/metrics/DataTable.module.css index b8b203c4..b21b92b9 100644 --- a/components/metrics/DataTable.module.css +++ b/components/metrics/DataTable.module.css @@ -1,14 +1,15 @@ .table { position: relative; + height: 100%; font-size: var(--font-size-small); - display: flex; - flex-direction: column; - flex: 1; + display: grid; + grid-template-rows: fit-content(100%) auto; overflow: hidden; } .body { position: relative; + height: 100%; overflow: auto; } diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js index 5599bc2e..e497a25e 100644 --- a/components/metrics/EventsTable.js +++ b/components/metrics/EventsTable.js @@ -19,7 +19,7 @@ export default function EventsTable({ websiteId, ...props }) { function handleDataLoad(data) { setEventTypes([...new Set(data.map(({ x }) => x.split('\t')[0]))]); - props.onDataLoad(data); + props.onDataLoad?.(data); } return ( diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 25bb4a08..95eb00c3 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -17,7 +17,6 @@ import styles from './MetricsTable.module.css'; export default function MetricsTable({ websiteId, - websiteDomain, type, className, dataFilter, @@ -42,7 +41,6 @@ export default function MetricsTable({ type, start_at: +startDate, end_at: +endDate, - domain: websiteDomain, url, }, onDataLoad, diff --git a/components/metrics/MetricsTable.module.css b/components/metrics/MetricsTable.module.css index e93f536e..d3a70866 100644 --- a/components/metrics/MetricsTable.module.css +++ b/components/metrics/MetricsTable.module.css @@ -1,6 +1,7 @@ .container { position: relative; min-height: 430px; + height: 100%; font-size: var(--font-size-small); display: flex; flex-direction: column; diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js index 63add268..8324f686 100644 --- a/components/metrics/RealtimeLog.js +++ b/components/metrics/RealtimeLog.js @@ -2,11 +2,11 @@ import React, { useMemo, useState } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import { FixedSizeList } from 'react-window'; import firstBy from 'thenby'; -import { format } from 'date-fns'; import Icon from 'components/common/Icon'; import Tag from 'components/common/Tag'; import Dot from 'components/common/Dot'; import FilterButtons from 'components/common/FilterButtons'; +import NoData from 'components/common/NoData'; import { devices } from 'components/messages'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; @@ -15,8 +15,8 @@ import Bolt from 'assets/bolt.svg'; import Visitor from 'assets/visitor.svg'; import Eye from 'assets/eye.svg'; import { stringToColor } from 'lib/format'; +import { dateFormat } from 'lib/date'; import styles from './RealtimeLog.module.css'; -import NoData from '../common/NoData'; const TYPE_ALL = 0; const TYPE_PAGEVIEW = 1; @@ -129,7 +129,12 @@ export default function RealtimeLog({ data, websites, websiteId }) { id="message.log.visitor" defaultMessage="Visitor from {country} using {browser} on {os} {device}" values={{ - country: {countryNames[country]}, + country: ( + + {countryNames[country] || + intl.formatMessage({ id: 'label.unknown', defaultMessage: 'Unknown' })} + + ), browser: {BROWSERS[browser]}, os: {os}, device: {intl.formatMessage(devices[device])?.toLowerCase()}, @@ -140,7 +145,7 @@ export default function RealtimeLog({ data, websites, websiteId }) { } function getTime({ created_at }) { - return format(new Date(created_at), 'h:mm:ss'); + return dateFormat(new Date(created_at), 'pp', locale); } function getColor(row) { @@ -176,9 +181,11 @@ export default function RealtimeLog({ data, websites, websiteId }) {
{logs?.length === 0 && } - - {Row} - + {logs?.length > 0 && ( + + {Row} + + )}
); diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css index 6fb09a64..7c07d017 100644 --- a/components/metrics/RealtimeLog.module.css +++ b/components/metrics/RealtimeLog.module.css @@ -1,6 +1,9 @@ .table { font-size: var(--font-size-xsmall); overflow: hidden; + height: 100%; + display: grid; + grid-template-rows: fit-content(100%) fit-content(100%) auto; } .header { @@ -21,6 +24,7 @@ .body { overflow: auto; + height: 100%; } .icon { diff --git a/components/metrics/ReferrersTable.js b/components/metrics/ReferrersTable.js index 2d51ab74..cbd4c9ba 100644 --- a/components/metrics/ReferrersTable.js +++ b/components/metrics/ReferrersTable.js @@ -42,7 +42,6 @@ export default function ReferrersTable({ websiteId, websiteDomain, showFilters, type="referrer" metric={} websiteId={websiteId} - websiteDomain={websiteDomain} dataFilter={refFilter} filterOptions={{ domain: websiteDomain, diff --git a/lib/constants.js b/lib/constants.js index 0d14448b..95421b22 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -80,7 +80,7 @@ export const POSTGRESQL_DATE_FORMATS = { year: 'YYYY-01-01', }; -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 DOMAIN_REGEX = /^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/; export const DESKTOP_SCREEN_WIDTH = 1920; export const LAPTOP_SCREEN_WIDTH = 1024; diff --git a/lib/date.js b/lib/date.js index 50d623bb..e166563b 100644 --- a/lib/date.js +++ b/lib/date.js @@ -23,7 +23,10 @@ import { differenceInCalendarDays, differenceInCalendarMonths, differenceInCalendarYears, + format, } from 'date-fns'; +import { enUS } from 'date-fns/locale'; +import { dateLocales } from 'lib/lang'; export function getTimezone() { return moment.tz.guess(); @@ -150,3 +153,16 @@ export function getDateLength(startDate, endDate, unit) { const [diff] = dateFuncs[unit]; return diff(endDate, startDate) + 1; } + +export const customFormats = { + 'en-US': { + p: 'ha', + pp: 'h:mm:ss', + }, +}; + +export function dateFormat(date, str, locale = 'en-US') { + return format(date, customFormats?.[locale]?.[str] || str, { + locale: dateLocales[locale] || enUS, + }); +} diff --git a/lib/lang.js b/lib/lang.js index 3bb6ffe8..02ec1c3c 100644 --- a/lib/lang.js +++ b/lib/lang.js @@ -1,4 +1,3 @@ -import { format } from 'date-fns'; import { cs, da, @@ -118,11 +117,6 @@ export const dateLocales = { 'it-IT': it, }; -const timeFormats = { - // https://date-fns.org/v2.17.0/docs/format - 'en-US': 'ha', -}; - export const menuOptions = [ { label: '中文', value: 'zh-CN', display: 'cn' }, { label: '中文(繁體)', value: 'zh-TW', display: 'tw' }, @@ -153,11 +147,3 @@ export const menuOptions = [ { label: 'Türkçe', value: 'tr-TR', display: 'tr' }, { label: 'українська', value: 'uk-UA', display: 'uk' }, ]; - -export function dateFormat(date, str, locale) { - return format(date, str, { locale: dateLocales[locale] || enUS }); -} - -export function timeFormat(date, locale = 'en-US') { - return format(date, timeFormats[locale] || 'p', { locale: dateLocales[locale] }); -} diff --git a/lib/queries.js b/lib/queries.js index df23bde8..48f9f265 100644 --- a/lib/queries.js +++ b/lib/queries.js @@ -428,7 +428,7 @@ export function getPageviewMetrics(website_id, start_at, end_at, field, table, f if (domain) { domainFilter = `and referrer not like $${params.length + 1} and referrer not like '/%'`; - params.push(`%${domain}%`); + params.push(`%://${domain}/%`); } if (url) { diff --git a/package.json b/package.json index a512a5ad..485cc221 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "umami", - "version": "1.13.0", + "version": "1.14.0", "description": "A simple, fast, website analytics alternative to Google Analytics. ", "author": "Mike Cao ", "license": "MIT", - "homepage": "https://github.com/mikecao/umami", + "homepage": "https://umami.is", "repository": { "type": "git", "url": "https://github.com/mikecao/umami.git" @@ -77,6 +77,7 @@ "moment-timezone": "^0.5.32", "next": "^10.0.7", "prompts": "2.4.0", + "prop-types": "^15.7.2", "react": "^17.0.1", "react-dom": "^17.0.1", "react-intl": "^5.12.3", diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js index ef736ee0..3e9b9925 100644 --- a/pages/api/website/[id]/metrics.js +++ b/pages/api/website/[id]/metrics.js @@ -1,6 +1,5 @@ -import { getPageviewMetrics, getSessionMetrics } from 'lib/queries'; -import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response'; -import { DOMAIN_REGEX } from 'lib/constants'; +import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'lib/queries'; +import { ok, methodNotAllowed, unauthorized, badRequest } from 'lib/response'; import { allowQuery } from 'lib/auth'; const sessionColumns = ['browser', 'os', 'device', 'country']; @@ -31,11 +30,7 @@ export default async (req, res) => { return unauthorized(res); } - const { id, type, start_at, end_at, domain, url } = req.query; - - if (domain && !DOMAIN_REGEX.test(domain)) { - return badRequest(res); - } + const { id, type, start_at, end_at, url } = req.query; const websiteId = +id; const startDate = new Date(+start_at); @@ -47,7 +42,18 @@ export default async (req, res) => { return ok(res, data); } - if (type === 'event' || pageviewColumns.includes(type)) { + if (pageviewColumns.includes(type) || type === 'event') { + let domain; + if (type === 'referrer') { + const website = getWebsiteById(websiteId); + + if (!website) { + return badRequest(res); + } + + domain = website.domain; + } + const data = await getPageviewMetrics( websiteId, startDate, @@ -55,7 +61,7 @@ export default async (req, res) => { getColumn(type), getTable(type), { - domain: type !== 'event' && domain, + domain, url: type !== 'url' && url, }, ); diff --git a/styles/index.css b/styles/index.css index e65608b6..de6f9ce8 100644 --- a/styles/index.css +++ b/styles/index.css @@ -14,6 +14,7 @@ body { font-size: var(--font-size-normal); color: var(--gray900); background: var(--gray75); + overflow-y: overlay; } .zh-CN {