Added filter buttons for realtime.

This commit is contained in:
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,
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}>

View File

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

View File

@ -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}

View File

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

View File

@ -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}

View File

@ -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>

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

View File

@ -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} />

View File

@ -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>

View File

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

View File

@ -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;