mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-18 07:13:37 +01:00
Added filter buttons for realtime.
This commit is contained in:
parent
5a73c224b7
commit
f1624780ee
11
components/common/FilterButtons.js
Normal file
11
components/common/FilterButtons.js
Normal file
@ -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 (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
}
|
@ -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}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.body} style={{ height }}>
|
||||
{data?.length === 0 && <NoData />}
|
||||
{virtualize && data.length > 0 ? (
|
||||
<FixedSizeList height={height} itemCount={data.length} itemSize={30}>
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
.body {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -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({
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{!data && loading && <Loading />}
|
||||
{error && <ErrorMessage />}
|
||||
{data && !error && (
|
||||
<DataTable
|
||||
{...props}
|
||||
title={title}
|
||||
data={filteredData}
|
||||
metric={metric}
|
||||
className={className}
|
||||
renderLabel={renderLabel}
|
||||
height={height}
|
||||
virtualize={virtualize}
|
||||
/>
|
||||
)}
|
||||
{data && !error && <DataTable {...props} data={filteredData} className={className} />}
|
||||
<div className={styles.footer}>
|
||||
{limit && (
|
||||
{data && !error && limit && (
|
||||
<Link
|
||||
icon={<Arrow />}
|
||||
href={router.pathname}
|
||||
|
@ -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 (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
};
|
||||
|
@ -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 (
|
||||
<PageviewsChart
|
||||
{...props}
|
||||
height={300}
|
||||
height={200}
|
||||
unit={unit}
|
||||
data={chartData}
|
||||
animationDuration={animationDuration}
|
||||
|
@ -1,10 +1,12 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { FormattedMessage } 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 useLocale from 'hooks/useLocale';
|
||||
import useCountryNames from 'hooks/useCountryNames';
|
||||
import { BROWSERS } from 'lib/constants';
|
||||
@ -13,11 +15,11 @@ import Visitor from 'assets/visitor.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
import Dot from '../common/Dot';
|
||||
|
||||
const TYPE_PAGEVIEW = 0;
|
||||
const TYPE_SESSION = 1;
|
||||
const TYPE_EVENT = 2;
|
||||
const TYPE_ALL = 0;
|
||||
const TYPE_PAGEVIEW = 1;
|
||||
const TYPE_SESSION = 2;
|
||||
const TYPE_EVENT = 3;
|
||||
|
||||
const TYPE_ICONS = {
|
||||
[TYPE_PAGEVIEW]: <Eye />,
|
||||
@ -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: <FormattedMessage id="label.all" defaultMessage="All" />,
|
||||
value: TYPE_ALL,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
|
||||
value: TYPE_PAGEVIEW,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />,
|
||||
value: TYPE_SESSION,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.events" defaultMessage="Events" />,
|
||||
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 }) {
|
||||
<FormattedMessage
|
||||
id="message.log.visitor"
|
||||
defaultMessage="Visitor from {country} using {browser} on {os} {device}"
|
||||
values={{ country: countryNames[country], browser: BROWSERS[browser], os, device }}
|
||||
values={{
|
||||
country: <b>{countryNames[country]}</b>,
|
||||
browser: BROWSERS[browser],
|
||||
os,
|
||||
device,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -123,6 +154,7 @@ export default function RealtimeLog({ data, websites }) {
|
||||
|
||||
return (
|
||||
<div className={styles.table}>
|
||||
<FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
|
||||
<div className={styles.header}>
|
||||
<FormattedMessage id="label.realtime-logs" defaultMessage="Realtime logs" />
|
||||
</div>
|
||||
|
100
components/metrics/RealtimeViews.js
Normal file
100
components/metrics/RealtimeViews.js
Normal file
@ -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: <FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />,
|
||||
value: FILTER_REFERRERS,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="metrics.pages" defaultMessage="Pages" />,
|
||||
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 (
|
||||
<>
|
||||
<FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
|
||||
{filter === FILTER_REFERRERS && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
data={referrers}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
{filter === FILTER_PAGES && (
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.pages" defaultMessage="Pages" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
data={pages}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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 (
|
||||
<ButtonLayout>
|
||||
<ButtonGroup size="xsmall" items={buttons} selectedItem={selected} onClick={onClick} />
|
||||
</ButtonLayout>
|
||||
);
|
||||
};
|
||||
|
@ -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 }) => <span className={locale}>{countryNames[x]}</span>,
|
||||
[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() {
|
||||
<GridLayout>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} lg={4}>
|
||||
<DataTable
|
||||
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
|
||||
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
|
||||
data={referrers}
|
||||
height={400}
|
||||
/>
|
||||
<RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<RealtimeLog data={realtimeData} websites={websites} />
|
||||
|
@ -173,7 +173,14 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
contentClassName={styles.content}
|
||||
menu={menuOptions}
|
||||
>
|
||||
<DetailsComponent {...tableProps} height={500} limit={false} showFilters virtualize />
|
||||
<DetailsComponent
|
||||
{...tableProps}
|
||||
height={500}
|
||||
limit={false}
|
||||
animte={false}
|
||||
showFilters
|
||||
virtualize
|
||||
/>
|
||||
</MenuLayout>
|
||||
)}
|
||||
</Page>
|
||||
|
@ -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 });
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
Loading…
Reference in New Issue
Block a user