From b682e41afff4cfeb70d3d121c900790ef77a8495 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 9 Oct 2020 02:56:15 -0700 Subject: [PATCH] RealtimeLog component. --- components/common/Table.js | 4 +- components/common/Table.module.css | 1 + components/metrics/RealtimeChart.js | 114 ++++++++-------------- components/metrics/RealtimeLog.js | 95 ++++++++++++++++++ components/metrics/RealtimeLog.module.css | 8 ++ components/pages/RealtimeDashboard.js | 64 ++++-------- 6 files changed, 163 insertions(+), 123 deletions(-) create mode 100644 components/metrics/RealtimeLog.js create mode 100644 components/metrics/RealtimeLog.module.css diff --git a/components/common/Table.js b/components/common/Table.js index a8a42b78..61eb4f6e 100644 --- a/components/common/Table.js +++ b/components/common/Table.js @@ -3,13 +3,13 @@ import classNames from 'classnames'; import NoData from 'components/common/NoData'; import styles from './Table.module.css'; -export default function Table({ columns, rows, empty }) { +export default function Table({ className, columns, rows, empty }) { if (empty && rows.length === 0) { return empty; } return ( -
+
{columns.map(({ key, label, className, style, header }) => (
{ - const { - data: { datasets }, - } = chart; + data.reduce((obj, val) => { + const { created_at } = val; + const t = startOfMinute(parseISO(created_at)); + if (t.getTime() > last) { + obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; + arr.push(obj); + last = t; + } else { + obj.y += 1; + } + return obj; + }, {}); - datasets[0].data = data.uniques; - datasets[0].label = intl.formatMessage({ - id: 'metrics.unique-visitors', - defaultMessage: 'Unique visitors', - }); - datasets[1].data = data.pageviews; - datasets[1].label = intl.formatMessage({ - id: 'metrics.page-views', - defaultMessage: 'Page views', - }); - - chart.update(); - }; - - if (!data) { - return null; - } - - return ( - - ); + return arr; +} + +export default function RealtimeChart({ data, ...props }) { + const chartData = useMemo(() => { + if (data) { + const endDate = startOfMinute(new Date()); + const startDate = subMinutes(endDate, 30); + const unit = 'minute'; + + return { + pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), + sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), + }; + } + return { pageviews: [], sessions: [] }; + }, [data]); + + return ; } diff --git a/components/metrics/RealtimeLog.js b/components/metrics/RealtimeLog.js new file mode 100644 index 00000000..b3a82050 --- /dev/null +++ b/components/metrics/RealtimeLog.js @@ -0,0 +1,95 @@ +import React, { useMemo } from 'react'; +import { FormattedMessage } from 'react-intl'; +import firstBy from 'thenby'; +import { format } from 'date-fns'; +import Table from 'components/common/Table'; +import styles from './RealtimeLog.module.css'; +import useLocale from '../../hooks/useLocale'; +import useCountryNames from '../../hooks/useCountryNames'; +import { BROWSERS } from '../../lib/constants'; + +export default function RealtimeLog({ data, websites }) { + const [locale] = useLocale(); + const countryNames = useCountryNames(locale); + const logs = useMemo(() => { + const { pageviews, sessions, events } = data; + return [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1)); + }, [data]); + + const columns = [ + { + key: 'time', + label: , + className: 'col', + render: ({ created_at }) => format(new Date(created_at), 'H:mm:ss'), + }, + { + key: 'website', + label: , + className: 'col', + render: getWebsite, + }, + { + key: 'type', + label: , + className: 'col', + render: getType, + }, + { + key: 'type', + className: 'col', + render: getDescription, + }, + ]; + + function getType({ view_id, session_id, event_id }) { + if (event_id) { + return ; + } + if (view_id) { + return ; + } + if (session_id) { + return ; + } + return null; + } + + function getWebsite({ website_id }) { + return websites.find(n => n.website_id === website_id)?.name; + } + + function getDescription({ + event_type, + event_value, + view_id, + session_id, + url, + browser, + os, + country, + device, + }) { + if (event_type) { + return `${event_type}:${event_value}`; + } + if (view_id) { + return url; + } + if (session_id) { + return ( + + ); + } + } + + return ( +
+ + + ); +} diff --git a/components/metrics/RealtimeLog.module.css b/components/metrics/RealtimeLog.module.css new file mode 100644 index 00000000..66a2efd3 --- /dev/null +++ b/components/metrics/RealtimeLog.module.css @@ -0,0 +1,8 @@ +.table { + font-size: var(--font-size-small); +} + +.row { + display: flex; + border-bottom: 1px solid var(--gray300); +} diff --git a/components/pages/RealtimeDashboard.js b/components/pages/RealtimeDashboard.js index 341b9e13..348b6642 100644 --- a/components/pages/RealtimeDashboard.js +++ b/components/pages/RealtimeDashboard.js @@ -1,37 +1,20 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useState, useEffect } from 'react'; import { FormattedMessage } from 'react-intl'; -import { subMinutes, startOfMinute, parseISO, format } from 'date-fns'; +import { subMinutes, startOfMinute } from 'date-fns'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import DropDown from 'components/common/DropDown'; import useFetch from 'hooks/useFetch'; -import PageviewsChart from '../metrics/PageviewsChart'; -import { getDateArray } from '../../lib/date'; +import RealtimeChart from '../metrics/RealtimeChart'; +import RealtimeLog from '../metrics/RealtimeLog'; + +const REALTIME_RANGE = 30; +const REALTIME_INTERVAL = 5000; function filterTime(data, time) { return data.filter(({ created_at }) => new Date(created_at).getTime() >= time); } -function mapData(data) { - let last = 0; - const arr = []; - - data.reduce((obj, val) => { - const { created_at } = val; - const t = startOfMinute(parseISO(created_at)); - if (t.getTime() > last) { - obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; - arr.push(obj); - last = t; - } else { - obj.y += 1; - } - return obj; - }, {}); - - return arr; -} - export default function RealtimeDashboard() { const [data, setData] = useState(); const [website, setWebsite] = useState(); @@ -42,39 +25,25 @@ export default function RealtimeDashboard() { { type: 'update', start_at: lastTime }, { disabled: !init?.token, - interval: 5000, + interval: REALTIME_INTERVAL, headers: { 'x-umami-token': init?.token }, }, ); - const chartData = useMemo(() => { - if (data) { - const endDate = startOfMinute(new Date()); - const startDate = subMinutes(endDate, 30); - const unit = 'minute'; - - return { - pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), - sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), - }; - } - return { pageviews: [], sessions: [] }; - }, [data]); - useEffect(() => { if (init && !data) { setData(init.data); } else if (updates) { const { pageviews, sessions, events } = updates; - const minTime = subMinutes(startOfMinute(new Date()), 30).getTime(); + const minTime = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(); setData(state => ({ - pageviews: filterTime(state.pageviews, minTime).concat(filterTime(pageviews, lastTime)), - sessions: filterTime(state.sessions, minTime).concat(filterTime(sessions, lastTime)), - events: filterTime(state.events, minTime).concat(filterTime(events, lastTime)), + pageviews: filterTime(state.pageviews.concat(pageviews), minTime), + sessions: filterTime(state.sessions.concat(sessions), minTime), + events: filterTime(state.events.concat(events), minTime), })); } setLastTime(Date.now()); - }, [updates, init]); + }, [init, updates]); if (!init || loading || !data) { return null; @@ -99,7 +68,12 @@ export default function RealtimeDashboard() { - + +
+
+ +
+
); }