+
{user && (
-
-
-
+
);
}
diff --git a/components/layout/Header.module.css b/components/layout/Header.module.css
index b7fdc62c..77bb8b8f 100644
--- a/components/layout/Header.module.css
+++ b/components/layout/Header.module.css
@@ -1,6 +1,19 @@
+.navbar {
+ align-items: stretch;
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.burger {
+ display: none;
+}
+
.header {
display: flex;
min-height: 100px;
+ width: 100%;
}
.title {
@@ -15,6 +28,15 @@
}
.nav {
+ display: flex;
+ align-items: center;
+ font-size: var(--font-size-normal);
+ font-weight: 600;
+ width: 100%;
+ justify-content: space-between;
+}
+
+.items {
display: flex;
justify-content: center;
align-items: center;
@@ -33,17 +55,77 @@
}
@media only screen and (max-width: 992px) {
- .title {
- justify-content: center;
+ .nav {
+ font-size: var(--font-size-large);
+ justify-content: space-between;
+ margin: 20px 0;
+ }
+ .items {
+ flex-wrap: wrap;
+ }
+}
+
+@media only screen and (max-width: 768px) {
+ .header {
+ padding: 0 15px;
}
.nav {
- font-size: var(--font-size-large);
+ font-size: var(--font-size-normal);
+ flex-wrap: wrap;
justify-content: center;
- padding: 20px 0;
+ flex-direction: column;
+ position: relative;
}
- .buttons {
- justify-content: center;
+ .items {
+ display: flex;
+ justify-content: unset;
+ align-items: left;
+ font-size: var(--font-size-normal);
+ font-weight: 600;
+ }
+
+ .items > div {
+ display: none;
+ }
+
+ .header .active {
+ display: inherit;
+ width: 100%;
+ }
+
+ .items a {
+ width: 100%;
+ }
+
+ .burger {
+ display: block;
+ /* color: #4a4a4a; */
+ cursor: pointer;
+ height: 3.25rem;
+ width: 3.25rem;
+ margin-left: auto;
+ position: absolute;
+ right: 0;
+ top: 0;
+ }
+
+ .burger span {
+ transform: translateX(-50%);
+ padding: 1px 0px;
+ margin: 6px 0;
+ width: 20px;
+ display: block;
+ background-color: white;
+ }
+
+ .burger div {
+ height: 100%;
+ color: white;
+ text-align: center;
+ margin: auto;
+ font-size: 1.5rem;
+ transform: translateX(-50%);
}
}
diff --git a/components/layout/Layout.js b/components/layout/Layout.js
index b16a0717..3b68a581 100644
--- a/components/layout/Layout.js
+++ b/components/layout/Layout.js
@@ -8,11 +8,6 @@ export default function Layout({ title, children, header = true, footer = true }
<>
umami{title && ` - ${title}`}
-
-
{header &&
}
{children}
diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js
index f42cf73f..3558e593 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 } 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';
@@ -40,26 +40,35 @@ export default function BarChart({
function renderXLabel(label, index, values) {
if (loading) return '';
const d = new Date(values[index].value);
- const w = canvas.current.width;
+ const sw = canvas.current.width / window.devicePixelRatio;
switch (unit) {
case 'minute':
- return index % 2 === 0 ? dateFormat(d, 'h:mm', locale) : '';
+ return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : '';
case 'hour':
- return dateFormat(d, 'ha', locale);
+ return dateFormat(d, 'p', locale);
case 'day':
- if (records > 31) {
- if (w <= 500) {
+ if (records > 25) {
+ if (sw <= 275) {
return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : '';
}
- return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
+ if (sw <= 550) {
+ return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : '';
+ }
+ if (sw <= 700) {
+ return index % 2 === 0 ? dateFormat(d, 'M/d', locale) : '';
+ }
+ return dateFormat(d, 'MMM d', locale);
}
- if (w <= 500) {
+ if (sw <= 375) {
return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : '';
}
+ if (sw <= 425) {
+ return dateFormat(d, 'MMM d', locale);
+ }
return dateFormat(d, 'EEE M/d', locale);
case 'month':
- if (w <= 660) {
+ if (sw <= 330) {
return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : '';
}
return dateFormat(d, 'MMM', locale);
@@ -93,9 +102,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';
}
}
@@ -131,6 +140,7 @@ export default function BarChart({
minRotation: 0,
maxRotation: 0,
fontColor: colors.text,
+ autoSkipPadding: 1,
},
gridLines: {
display: false,
@@ -175,6 +185,7 @@ export default function BarChart({
options.scales.xAxes[0].ticks.callback = renderXLabel;
options.scales.xAxes[0].ticks.fontColor = colors.text;
options.scales.yAxes[0].ticks.fontColor = colors.text;
+ options.scales.yAxes[0].ticks.precision = 0;
options.scales.yAxes[0].gridLines.color = colors.line;
options.scales.yAxes[0].gridLines.zeroLineColor = colors.zeroLine;
options.animation.duration = animationDuration;
diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js
index 59d17dfb..a5c9c801 100644
--- a/components/metrics/CountriesTable.js
+++ b/components/metrics/CountriesTable.js
@@ -10,7 +10,11 @@ export default function CountriesTable({ websiteId, onDataLoad, ...props }) {
const countryNames = useCountryNames(locale);
function renderLabel({ x }) {
- return
{countryNames[x]}
;
+ return (
+
+ {countryNames[x] ?? }
+
+ );
}
return (
diff --git a/components/metrics/DataTable.js b/components/metrics/DataTable.js
index 5cf9ddb4..d7e58cf7 100644
--- a/components/metrics/DataTable.js
+++ b/components/metrics/DataTable.js
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { FixedSizeList } from 'react-window';
import { useSpring, animated, config } from 'react-spring';
import classNames from 'classnames';
+import { FormattedMessage } from 'react-intl';
import NoData from 'components/common/NoData';
import { formatNumber, formatLongNumber } from 'lib/format';
import styles from './DataTable.module.css';
@@ -27,7 +28,11 @@ export default function DataTable({
return (
+ }
value={value}
percent={percent}
animate={animate && !virtualize}
diff --git a/components/metrics/DataTable.module.css b/components/metrics/DataTable.module.css
index 79a60577..b21b92b9 100644
--- a/components/metrics/DataTable.module.css
+++ b/components/metrics/DataTable.module.css
@@ -1,13 +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/DevicesTable.js b/components/metrics/DevicesTable.js
index 52b6b5fc..3073cf8b 100644
--- a/components/metrics/DevicesTable.js
+++ b/components/metrics/DevicesTable.js
@@ -1,6 +1,5 @@
import React from 'react';
import MetricsTable from './MetricsTable';
-import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages';
@@ -12,7 +11,6 @@ export default function DevicesTable({ websiteId, ...props }) {
type="device"
metric={
}
websiteId={websiteId}
- dataFilter={deviceFilter}
renderLabel={({ x }) => getDeviceMessage(x)}
/>
);
diff --git a/components/metrics/EventsTable.js b/components/metrics/EventsTable.js
index c415a3e9..e497a25e 100644
--- a/components/metrics/EventsTable.js
+++ b/components/metrics/EventsTable.js
@@ -1,23 +1,51 @@
-import React from 'react';
+import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import Tag from 'components/common/Tag';
+import DropDown from 'components/common/DropDown';
+import { eventTypeFilter } from 'lib/filters';
+import styles from './EventsTable.module.css';
+
+const EVENT_FILTER_DEFAULT = {
+ value: 'EVENT_FILTER_DEFAULT',
+ label:
,
+};
export default function EventsTable({ websiteId, ...props }) {
+ const [eventType, setEventType] = useState(EVENT_FILTER_DEFAULT.value);
+ const [eventTypes, setEventTypes] = useState([]);
+
+ const dropDownOptions = [EVENT_FILTER_DEFAULT, ...eventTypes.map(t => ({ value: t, label: t }))];
+
+ function handleDataLoad(data) {
+ setEventTypes([...new Set(data.map(({ x }) => x.split('\t')[0]))]);
+ props.onDataLoad?.(data);
+ }
+
return (
-
}
- type="event"
- metric={
}
- websiteId={websiteId}
- renderLabel={({ x }) =>
}
- />
+ <>
+ {eventTypes?.length > 1 && (
+
+
+
+ )}
+
}
+ type="event"
+ metric={
}
+ websiteId={websiteId}
+ dataFilter={eventTypeFilter}
+ filterOptions={eventType === EVENT_FILTER_DEFAULT.value ? [] : [eventType]}
+ renderLabel={({ x }) =>
}
+ onDataLoad={handleDataLoad}
+ />
+ >
);
}
const Label = ({ value }) => {
- const [event, label] = value.split(':');
+ const [event, label] = value.split('\t');
return (
<>
{event}
diff --git a/components/metrics/EventsTable.module.css b/components/metrics/EventsTable.module.css
new file mode 100644
index 00000000..259a4bbb
--- /dev/null
+++ b/components/metrics/EventsTable.module.css
@@ -0,0 +1,6 @@
+.filter {
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ margin-bottom: 15px;
+}
diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js
index 886ee5f0..945cd5e2 100644
--- a/components/metrics/MetricsBar.js
+++ b/components/metrics/MetricsBar.js
@@ -41,6 +41,7 @@ export default function MetricsBar({ websiteId, className }) {
}
const { pageviews, uniques, bounces, totaltime } = data || {};
+ const num = Math.min(uniques, bounces);
return (
@@ -60,7 +61,7 @@ export default function MetricsBar({ websiteId, className }) {
/>
}
- value={pageviews ? (bounces / pageviews) * 100 : 0}
+ value={uniques ? (num / uniques) * 100 : 0}
format={n => Number(n).toFixed(0) + '%'}
/>
div:nth-child(n + 3) {
- display: none;
+ overflow: auto;
}
}
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/OSTable.js b/components/metrics/OSTable.js
index c1790e17..c77ae074 100644
--- a/components/metrics/OSTable.js
+++ b/components/metrics/OSTable.js
@@ -1,6 +1,5 @@
import React from 'react';
import MetricsTable from './MetricsTable';
-import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function OSTable({ websiteId, ...props }) {
@@ -11,7 +10,6 @@ export default function OSTable({ websiteId, ...props }) {
type="os"
metric={}
websiteId={websiteId}
- dataFilter={osFilter}
/>
);
}
diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js
index 18f064e8..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;
@@ -29,7 +29,7 @@ const TYPE_ICONS = {
[TYPE_EVENT]: ,
};
-export default function RealtimeLog({ data, websites }) {
+export default function RealtimeLog({ data, websites, websiteId }) {
const intl = useIntl();
const [locale] = useLocale();
const countryNames = useCountryNames(locale);
@@ -88,7 +88,7 @@ export default function RealtimeLog({ data, websites }) {
}
function getWebsite({ website_id }) {
- return websites.find(n => n.website_id === website_id)?.name;
+ return websites.find(n => n.website_id === website_id);
}
function getDetail({
@@ -101,6 +101,7 @@ export default function RealtimeLog({ data, websites }) {
os,
country,
device,
+ website_id,
}) {
if (event_type) {
return (
@@ -110,7 +111,17 @@ export default function RealtimeLog({ data, websites }) {
);
}
if (view_id) {
- return url;
+ const domain = getWebsite({ website_id })?.domain;
+ return (
+
+ {url}
+
+ );
}
if (session_id) {
return (
@@ -118,7 +129,12 @@ export default function RealtimeLog({ data, websites }) {
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()},
@@ -129,7 +145,7 @@ export default function RealtimeLog({ data, websites }) {
}
function getTime({ created_at }) {
- return format(new Date(created_at), 'h:mm:ss');
+ return dateFormat(new Date(created_at), 'pp', locale);
}
function getColor(row) {
@@ -150,7 +166,9 @@ export default function RealtimeLog({ data, websites }) {
{getDetail(row)}
-
{getWebsite(row)}
+ {!websiteId && websites.length > 1 && (
+
{getWebsite(row)?.domain}
+ )}
);
};
@@ -163,9 +181,11 @@ export default function RealtimeLog({ data, websites }) {