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