Added filter buttons for realtime.

pull/296/head
Mike Cao 2020-10-12 16:31:51 -07:00
parent 5a73c224b7
commit f1624780ee
13 changed files with 194 additions and 105 deletions

View 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>
);
}

View File

@ -12,7 +12,7 @@ export default function DataTable({
metric, metric,
className, className,
renderLabel, renderLabel,
height = 400, height,
animate = true, animate = true,
virtualize = false, virtualize = false,
}) { }) {
@ -49,7 +49,7 @@ export default function DataTable({
{metric} {metric}
</div> </div>
</div> </div>
<div className={styles.body}> <div className={styles.body} style={{ height }}>
{data?.length === 0 && <NoData />} {data?.length === 0 && <NoData />}
{virtualize && data.length > 0 ? ( {virtualize && data.length > 0 ? (
<FixedSizeList height={height} itemCount={data.length} itemSize={30}> <FixedSizeList height={height} itemCount={data.length} itemSize={30}>

View File

@ -8,7 +8,7 @@
.body { .body {
position: relative; position: relative;
flex: 1; overflow: auto;
} }
.header { .header {

View File

@ -17,16 +17,11 @@ import styles from './MetricsTable.module.css';
export default function MetricsTable({ export default function MetricsTable({
websiteId, websiteId,
websiteDomain, websiteDomain,
title,
metric,
type, type,
className, className,
dataFilter, dataFilter,
filterOptions, filterOptions,
limit, limit,
virtualize,
renderLabel,
height,
onDataLoad, onDataLoad,
...props ...props
}) { }) {
@ -71,20 +66,9 @@ export default function MetricsTable({
<div className={classNames(styles.container, className)}> <div className={classNames(styles.container, className)}>
{!data && loading && <Loading />} {!data && loading && <Loading />}
{error && <ErrorMessage />} {error && <ErrorMessage />}
{data && !error && ( {data && !error && <DataTable {...props} data={filteredData} className={className} />}
<DataTable
{...props}
title={title}
data={filteredData}
metric={metric}
className={className}
renderLabel={renderLabel}
height={height}
virtualize={virtualize}
/>
)}
<div className={styles.footer}> <div className={styles.footer}>
{limit && ( {data && !error && limit && (
<Link <Link
icon={<Arrow />} icon={<Arrow />}
href={router.pathname} href={router.pathname}

View File

@ -2,14 +2,15 @@ import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import Link from 'next/link'; import Link from 'next/link';
import ButtonGroup from 'components/common/ButtonGroup'; import FilterButtons from 'components/common/FilterButtons';
import ButtonLayout from 'components/layout/ButtonLayout';
import { urlFilter } from 'lib/filters'; import { urlFilter } from 'lib/filters';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import usePageQuery from 'hooks/usePageQuery'; import usePageQuery from 'hooks/usePageQuery';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import styles from './PagesTable.module.css'; import styles from './PagesTable.module.css';
export const FILTER_COMBINED = 0;
export const FILTER_RAW = 1;
export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) { export default function PagesTable({ websiteId, websiteDomain, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); const [filter, setFilter] = useState(FILTER_COMBINED);
const { 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>
);
};

View File

@ -2,7 +2,7 @@ import React, { useMemo, useRef } from 'react';
import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns'; import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns';
import PageviewsChart from './PageviewsChart'; import PageviewsChart from './PageviewsChart';
import { getDateArray } from 'lib/date'; import { getDateArray } from 'lib/date';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants';
function mapData(data) { function mapData(data) {
let last = 0; let last = 0;
@ -26,7 +26,7 @@ function mapData(data) {
export default function RealtimeChart({ data, unit, ...props }) { export default function RealtimeChart({ data, unit, ...props }) {
const endDate = startOfMinute(new Date()); const endDate = startOfMinute(new Date());
const startDate = subMinutes(endDate, 30); const startDate = subMinutes(endDate, REALTIME_RANGE);
const prevEndDate = useRef(endDate); const prevEndDate = useRef(endDate);
const chartData = useMemo(() => { const chartData = useMemo(() => {
@ -51,7 +51,7 @@ export default function RealtimeChart({ data, unit, ...props }) {
return ( return (
<PageviewsChart <PageviewsChart
{...props} {...props}
height={300} height={200}
unit={unit} unit={unit}
data={chartData} data={chartData}
animationDuration={animationDuration} animationDuration={animationDuration}

View File

@ -1,10 +1,12 @@
import React, { useMemo } from 'react'; import React, { useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } 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 { 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 FilterButtons from 'components/common/FilterButtons';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import { BROWSERS } from 'lib/constants'; import { BROWSERS } from 'lib/constants';
@ -13,11 +15,11 @@ 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 styles from './RealtimeLog.module.css'; import styles from './RealtimeLog.module.css';
import Dot from '../common/Dot';
const TYPE_PAGEVIEW = 0; const TYPE_ALL = 0;
const TYPE_SESSION = 1; const TYPE_PAGEVIEW = 1;
const TYPE_EVENT = 2; const TYPE_SESSION = 2;
const TYPE_EVENT = 3;
const TYPE_ICONS = { const TYPE_ICONS = {
[TYPE_PAGEVIEW]: <Eye />, [TYPE_PAGEVIEW]: <Eye />,
@ -28,11 +30,16 @@ const TYPE_ICONS = {
export default function RealtimeLog({ data, websites }) { export default function RealtimeLog({ data, websites }) {
const [locale] = useLocale(); const [locale] = useLocale();
const countryNames = useCountryNames(locale); const countryNames = useCountryNames(locale);
const [filter, setFilter] = useState(TYPE_ALL);
const logs = useMemo(() => { const logs = useMemo(() => {
const { pageviews, sessions, events } = data; const { pageviews, sessions, events } = data;
return [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); const logs = [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1));
}, [data]); if (filter) {
return logs.filter(row => getType(row) === filter);
}
return logs;
}, [data, filter]);
const uuids = useMemo(() => { const uuids = useMemo(() => {
return data.sessions.reduce((obj, { session_id, session_uuid }) => { return data.sessions.reduce((obj, { session_id, session_uuid }) => {
@ -41,6 +48,25 @@ export default function RealtimeLog({ data, websites }) {
}, {}); }, {});
}, [data]); }, [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 }) { function getType({ view_id, session_id, event_id }) {
if (event_id) { if (event_id) {
return TYPE_EVENT; return TYPE_EVENT;
@ -88,7 +114,12 @@ export default function RealtimeLog({ data, websites }) {
<FormattedMessage <FormattedMessage
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={{ 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 ( return (
<div className={styles.table}> <div className={styles.table}>
<FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
<div className={styles.header}> <div className={styles.header}>
<FormattedMessage id="label.realtime-logs" defaultMessage="Realtime logs" /> <FormattedMessage id="label.realtime-logs" defaultMessage="Realtime logs" />
</div> </div>

View 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}
/>
)}
</>
);
}

View File

@ -1,11 +1,13 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable'; import MetricsTable from './MetricsTable';
import ButtonGroup from 'components/common/ButtonGroup'; import FilterButtons from 'components/common/FilterButtons';
import ButtonLayout from 'components/layout/ButtonLayout';
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import { refFilter } from 'lib/filters'; 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 }) { export default function ReferrersTable({ websiteId, websiteDomain, showFilters, ...props }) {
const [filter, setFilter] = useState(FILTER_COMBINED); 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>
);
};

View File

@ -9,16 +9,14 @@ import RealtimeLog from 'components/metrics/RealtimeLog';
import RealtimeHeader from 'components/metrics/RealtimeHeader'; import RealtimeHeader from 'components/metrics/RealtimeHeader';
import WorldMap from 'components/common/WorldMap'; import WorldMap from 'components/common/WorldMap';
import DataTable from 'components/metrics/DataTable'; import DataTable from 'components/metrics/DataTable';
import RealtimeViews from 'components/metrics/RealtimeViews';
import useFetch from 'hooks/useFetch'; import useFetch from 'hooks/useFetch';
import useLocale from 'hooks/useLocale'; import useLocale from 'hooks/useLocale';
import useCountryNames from 'hooks/useCountryNames'; import useCountryNames from 'hooks/useCountryNames';
import { percentFilter } from 'lib/filters'; 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'; import styles from './RealtimeDashboard.module.css';
const REALTIME_RANGE = 30;
const REALTIME_INTERVAL = 3000;
function mergeData(state, data, time) { function mergeData(state, data, time) {
const ids = state.map(({ __id }) => __id); const ids = state.map(({ __id }) => __id);
return state return state
@ -43,7 +41,10 @@ export default function RealtimeDashboard() {
headers: { [TOKEN_HEADER]: init?.token }, 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(() => { const realtimeData = useMemo(() => {
if (data) { if (data) {
@ -83,38 +84,11 @@ export default function RealtimeDashboard() {
return []; return [];
}, [realtimeData?.sessions]); }, [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(() => { useEffect(() => {
if (init && !data) { if (init && !data) {
const { websites, data } = init; const { websites, data } = init;
const domains = init.websites.map(({ domain }) => domain);
setData({ websites, domains, ...data }); setData({ websites, ...data });
} }
}, [init]); }, [init]);
@ -158,12 +132,7 @@ export default function RealtimeDashboard() {
<GridLayout> <GridLayout>
<GridRow> <GridRow>
<GridColumn xs={12} lg={4}> <GridColumn xs={12} lg={4}>
<DataTable <RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
title={<FormattedMessage id="metrics.referrers" defaultMessage="Referrers" />}
metric={<FormattedMessage id="metrics.views" defaultMessage="Views" />}
data={referrers}
height={400}
/>
</GridColumn> </GridColumn>
<GridColumn xs={12} lg={8}> <GridColumn xs={12} lg={8}>
<RealtimeLog data={realtimeData} websites={websites} /> <RealtimeLog data={realtimeData} websites={websites} />

View File

@ -173,7 +173,14 @@ export default function WebsiteDetails({ websiteId }) {
contentClassName={styles.content} contentClassName={styles.content}
menu={menuOptions} menu={menuOptions}
> >
<DetailsComponent {...tableProps} height={500} limit={false} showFilters virtualize /> <DetailsComponent
{...tableProps}
height={500}
limit={false}
animte={false}
showFilters
virtualize
/>
</MenuLayout> </MenuLayout>
)} )}
</Page> </Page>

View File

@ -33,7 +33,7 @@ export async function allowQuery(req, skipToken) {
const website = await getWebsiteById(websiteId); const website = await getWebsiteById(websiteId);
if (website) { if (website) {
if (token && !skipToken) { if (token && token !== 'undefined' && !skipToken) {
return isValidToken(token, { website_id: websiteId }); return isValidToken(token, { website_id: websiteId });
} }

View File

@ -6,6 +6,15 @@ export const THEME_CONFIG = 'umami.theme';
export const VERSION_CHECK = 'umami.version-check'; export const VERSION_CHECK = 'umami.version-check';
export const TOKEN_HEADER = 'x-umami-token'; 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 = { export const THEME_COLORS = {
light: { light: {
primary: '#2680eb', primary: '#2680eb',
@ -52,12 +61,6 @@ export const EVENT_COLORS = [
'#ffec16', '#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 POSTGRESQL = 'postgresql';
export const MYSQL = 'mysql'; export const MYSQL = 'mysql';
@ -77,10 +80,6 @@ export const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01', 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 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; export const DESKTOP_SCREEN_WIDTH = 1920;