From 28921a7cd5cad000cb5dde100acc7e82162ed1ff Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Tue, 14 Feb 2023 17:23:20 -0800 Subject: [PATCH 1/8] Updates to realtime. Fixed refresh button. --- components/common/DateFilter.js | 89 ++++++++++------- components/common/FilterLink.js | 4 +- components/icons.ts | 4 + components/input/RefreshButton.js | 20 +--- components/layout/Grid.js | 13 +++ components/layout/Grid.module.css | 35 +++++++ components/messages.js | 14 ++- components/metrics/ActiveUsers.js | 1 + components/metrics/DatePickerForm.js | 2 +- components/metrics/FilterTags.js | 23 ++++- components/metrics/MetricsTable.js | 4 +- components/metrics/RealtimeHeader.js | 52 ---------- components/metrics/RealtimeHeader.module.css | 11 --- components/metrics/WebsiteChart.js | 46 ++------- components/metrics/WebsiteChart.module.css | 16 ++- .../pages/realtime/RealtimeDashboard.js | 99 ++++++++++--------- .../realtime/RealtimeDashboard.module.css | 9 ++ components/pages/realtime/RealtimeHeader.js | 44 +++++++++ .../pages/realtime/RealtimeHeader.module.css | 9 ++ .../realtime}/RealtimeLog.js | 45 +++++---- .../realtime}/RealtimeLog.module.css | 0 .../realtime}/RealtimeViews.js | 29 +++--- components/pages/websites/WebsiteDetails.js | 4 - components/pages/websites/WebsiteMenuView.js | 24 ++--- .../pages/websites/WebsiteMenuView.module.css | 14 --- components/pages/websites/WebsiteTableView.js | 56 +++++------ hooks/usePageQuery.js | 4 +- pages/api/realtime/init.ts | 3 +- pages/api/realtime/update.ts | 3 +- store/queries.js | 8 +- store/websites.js | 2 +- 31 files changed, 373 insertions(+), 314 deletions(-) create mode 100644 components/layout/Grid.js create mode 100644 components/layout/Grid.module.css delete mode 100644 components/metrics/RealtimeHeader.js delete mode 100644 components/metrics/RealtimeHeader.module.css create mode 100644 components/pages/realtime/RealtimeHeader.js create mode 100644 components/pages/realtime/RealtimeHeader.module.css rename components/{metrics => pages/realtime}/RealtimeLog.js (81%) rename components/{metrics => pages/realtime}/RealtimeLog.module.css (100%) rename components/{metrics => pages/realtime}/RealtimeViews.js (74%) diff --git a/components/common/DateFilter.js b/components/common/DateFilter.js index 77bab32f..a340fe48 100644 --- a/components/common/DateFilter.js +++ b/components/common/DateFilter.js @@ -1,68 +1,74 @@ -import { endOfYear, isSameDay } from 'date-fns'; import { useState } from 'react'; -import { Icon, Modal, Dropdown, Item } from 'react-basics'; -import { useIntl, defineMessages } from 'react-intl'; +import { Icon, Modal, Dropdown, Item, Text, Flexbox } from 'react-basics'; +import { useIntl } from 'react-intl'; +import { endOfYear, isSameDay } from 'date-fns'; import DatePickerForm from 'components/metrics/DatePickerForm'; import useLocale from 'hooks/useLocale'; -import { dateFormat } from 'lib/date'; -import Calendar from 'assets/calendar.svg'; +import { dateFormat, getDateRangeValues } from 'lib/date'; +import Icons from 'components/icons'; +import { labels } from 'components/messages'; +import useApi from 'hooks/useApi'; +import useDateRange from 'hooks/useDateRange'; -const messages = defineMessages({ - today: { id: 'label.today', defaultMessage: 'Today' }, - lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' }, - yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' }, - thisWeek: { id: 'label.this-week', defaultMessage: 'This week' }, - lastDays: { id: 'label.last-days', defaultMessage: 'Last {x} days' }, - thisMonth: { id: 'label.this-month', defaultMessage: 'This month' }, - thisYear: { id: 'label.this-year', defaultMessage: 'This year' }, - allTime: { id: 'label.all-time', defaultMessage: 'All time' }, - customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' }, -}); - -function DateFilter({ value, startDate, endDate, onChange, className }) { +function DateFilter({ websiteId, value, className }) { const { formatMessage } = useIntl(); + const { get } = useApi(); + const [dateRange, setDateRange] = useDateRange(websiteId); + const { startDate, endDate } = dateRange; const [showPicker, setShowPicker] = useState(false); + async function handleDateChange(value) { + if (value === 'all') { + const data = await get(`/websites/${websiteId}`); + + if (data) { + setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) }); + } + } else { + setDateRange(value); + } + } + const options = [ - { label: formatMessage(messages.today), value: '1day' }, + { label: formatMessage(labels.today), value: '1day' }, { - label: formatMessage(messages.lastHours, { x: 24 }), + label: formatMessage(labels.lastHours, { x: 24 }), value: '24hour', }, { - label: formatMessage(messages.yesterday), + label: formatMessage(labels.yesterday), value: '-1day', }, { - label: formatMessage(messages.thisWeek), + label: formatMessage(labels.thisWeek), value: '1week', divider: true, }, { - label: formatMessage(messages.lastDays, { x: 7 }), + label: formatMessage(labels.lastDays, { x: 7 }), value: '7day', }, { - label: formatMessage(messages.thisMonth), + label: formatMessage(labels.thisMonth), value: '1month', divider: true, }, { - label: formatMessage(messages.lastDays, { x: 30 }), + label: formatMessage(labels.lastDays, { x: 30 }), value: '30day', }, { - label: formatMessage(messages.lastDays, { x: 90 }), + label: formatMessage(labels.lastDays, { x: 90 }), value: '90day', }, - { label: formatMessage(messages.thisYear), value: '1year' }, + { label: formatMessage(labels.thisYear), value: '1year' }, { - label: formatMessage(messages.allTime), + label: formatMessage(labels.allTime), value: 'all', divider: true, }, { - label: formatMessage(messages.customRange), + label: formatMessage(labels.customRange), value: 'custom', divider: true, }, @@ -76,17 +82,17 @@ function DateFilter({ value, startDate, endDate, onChange, className }) { ); }; - const handleChange = async value => { + const handleChange = value => { if (value === 'custom') { setShowPicker(true); return; } - onChange(value); + handleDateChange(value); }; const handlePickerChange = value => { setShowPicker(false); - onChange(value); + handleDateChange(value); }; const handleClose = () => setShowPicker(false); @@ -98,9 +104,14 @@ function DateFilter({ value, startDate, endDate, onChange, className }) { items={options} renderValue={renderValue} value={value} + alignment="end" onChange={handleChange} > - {({ label, value }) => {label}} + {({ label, value, divider }) => ( + + {label} + + )} {showPicker && ( @@ -128,13 +139,15 @@ const CustomRange = ({ startDate, endDate, onClick }) => { } return ( - <> + - + - {dateFormat(startDate, 'd LLL y', locale)} - {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} - + + {dateFormat(startDate, 'd LLL y', locale)} + {!isSameDay(startDate, endDate) && ` — ${dateFormat(endDate, 'd LLL y', locale)}`} + + ); }; diff --git a/components/common/FilterLink.js b/components/common/FilterLink.js index f91d4764..eb410719 100644 --- a/components/common/FilterLink.js +++ b/components/common/FilterLink.js @@ -6,13 +6,13 @@ import { Icon, Icons } from 'react-basics'; import styles from './FilterLink.module.css'; export default function FilterLink({ id, value, label, externalUrl }) { - const { resolve, query } = usePageQuery(); + const { resolveUrl, query } = usePageQuery(); const active = query[id] !== undefined; const selected = query[id] === value; return (
- + state[`/websites/${websiteId}/stats`], [websiteId]); - const completed = useStore(selector); function handleClick() { - if (!loading && dateRange) { - setLoading(true); + if (!isLoading && dateRange) { if (/^\d+/.test(dateRange.value)) { setDateRange(websiteId, dateRange.value); } else { @@ -25,17 +19,13 @@ function RefreshButton({ websiteId }) { } } - useEffect(() => { - setLoading(false); - }, [completed]); - return ( - + ); } diff --git a/components/layout/Grid.js b/components/layout/Grid.js new file mode 100644 index 00000000..0276063b --- /dev/null +++ b/components/layout/Grid.js @@ -0,0 +1,13 @@ +import { Row, Column } from 'react-basics'; +import classNames from 'classnames'; +import styles from './Grid.module.css'; + +export function GridRow(props) { + const { className, ...otherProps } = props; + return ; +} + +export function GridColumn(props) { + const { className, ...otherProps } = props; + return ; +} diff --git a/components/layout/Grid.module.css b/components/layout/Grid.module.css new file mode 100644 index 00000000..20df43c9 --- /dev/null +++ b/components/layout/Grid.module.css @@ -0,0 +1,35 @@ +.col { + display: flex; + flex-direction: column; + padding: 20px; +} + +.row { + border-top: 1px solid var(--base300); + min-height: 430px; +} + +.row > .col { + border-left: 1px solid var(--base300); +} + +.row > .col:first-child { + border-left: 0; + padding-left: 0; +} + +.row > .col:last-child { + padding-right: 0; +} + +@media only screen and (max-width: 992px) { + .row { + border: 0; + } + + .row > .col { + border-top: 1px solid var(--base300); + border-left: 0; + padding: 20px 0; + } +} diff --git a/components/messages.js b/components/messages.js index c29432b1..ce506fd1 100644 --- a/components/messages.js +++ b/components/messages.js @@ -81,9 +81,21 @@ export const labels = defineMessages({ visitors: { id: 'label.visitors', defaultMessage: 'Visitors' }, filterCombined: { id: 'label.filter-combined', defaultMessage: 'Combined' }, filterRaw: { id: 'label.filter-raw', defaultMessage: 'Raw' }, - views: { id: 'label.views', defaultMessage: 'View' }, + views: { id: 'label.views', defaultMessage: 'Views' }, none: { id: 'label.none', defaultMessage: 'None' }, clearAll: { id: 'label.clear-all', defaultMessage: 'Clear all' }, + today: { id: 'label.today', defaultMessage: 'Today' }, + lastHours: { id: 'label.last-hours', defaultMessage: 'Last {x} hours' }, + yesterday: { id: 'label.yesterday', defaultMessage: 'Yesterday' }, + thisWeek: { id: 'label.this-week', defaultMessage: 'This week' }, + lastDays: { id: 'label.last-days', defaultMessage: 'Last {x} days' }, + thisMonth: { id: 'label.this-month', defaultMessage: 'This month' }, + thisYear: { id: 'label.this-year', defaultMessage: 'This year' }, + allTime: { id: 'label.all-time', defaultMessage: 'All time' }, + customRange: { id: 'label.custom-range', defaultMessage: 'Custom-range' }, + selectWebsite: { id: 'label.select-website', defaultMessage: 'Select website' }, + all: { id: 'label.all', defaultMessage: 'All' }, + sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, }); export const messages = defineMessages({ diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js index 664e23a9..cb3afe80 100644 --- a/components/metrics/ActiveUsers.js +++ b/components/metrics/ActiveUsers.js @@ -13,6 +13,7 @@ export default function ActiveUsers({ websiteId, value, refetchInterval = 60000 () => get(`/websites/${websiteId}/active`), { refetchInterval, + enabled: !!websiteId, }, ); diff --git a/components/metrics/DatePickerForm.js b/components/metrics/DatePickerForm.js index 17c7b5e5..5235d339 100644 --- a/components/metrics/DatePickerForm.js +++ b/components/metrics/DatePickerForm.js @@ -42,7 +42,7 @@ export default function DatePickerForm({ return (
- + diff --git a/components/metrics/FilterTags.js b/components/metrics/FilterTags.js index 0e68504b..37f5981c 100644 --- a/components/metrics/FilterTags.js +++ b/components/metrics/FilterTags.js @@ -1,26 +1,39 @@ import { useIntl } from 'react-intl'; -import classNames from 'classnames'; import { safeDecodeURI } from 'next-basics'; import { Button, Icon, Icons, Text } from 'react-basics'; import { labels } from 'components/messages'; +import usePageQuery from 'hooks/usePageQuery'; import styles from './FilterTags.module.css'; -export default function FilterTags({ className, params, onClick }) { +export default function FilterTags({ websiteId, params, onClick }) { const { formatMessage } = useIntl(); + const { + router, + resolveUrl, + query: { view }, + } = usePageQuery(); if (Object.keys(params).filter(key => params[key]).length === 0) { return null; } + function handleCloseFilter(param) { + if (param === null) { + router.push(`/websites/${websiteId}/?view=${view}`); + } else { + router.push(resolveUrl({ [param]: undefined })); + } + } + return ( -
+
{Object.keys(params).map(key => { if (!params[key]) { return null; } return (
-
); })} -
} + {filter === FILTER_REFERRERS && ( } - metric={} + title={formatMessage(labels.referrers)} + metric={formatMessage(labels.views)} renderLabel={renderLink} data={referrers} /> )} {filter === FILTER_PAGES && ( } - metric={} + title={formatMessage(labels.pages)} + metric={formatMessage(labels.views)} renderLabel={renderLink} data={pages} /> diff --git a/components/pages/websites/WebsiteDetails.js b/components/pages/websites/WebsiteDetails.js index 903a482a..d5b1c655 100644 --- a/components/pages/websites/WebsiteDetails.js +++ b/components/pages/websites/WebsiteDetails.js @@ -8,13 +8,10 @@ import WebsiteChart from 'components/metrics/WebsiteChart'; import useApi from 'hooks/useApi'; import usePageQuery from 'hooks/usePageQuery'; import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; -import { labels } from 'components/messages'; -import styles from './WebsiteDetails.module.css'; import WebsiteTableView from './WebsiteTableView'; import WebsiteMenuView from './WebsiteMenuView'; export default function WebsiteDetails({ websiteId }) { - const { formatMessage } = useIntl(); const { get, useQuery } = useApi(); const { data, isLoading, error } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), @@ -22,7 +19,6 @@ export default function WebsiteDetails({ websiteId }) { const [chartLoaded, setChartLoaded] = useState(false); const { - resolve, query: { view }, } = usePageQuery(); diff --git a/components/pages/websites/WebsiteMenuView.js b/components/pages/websites/WebsiteMenuView.js index 07a93268..c8b9db9c 100644 --- a/components/pages/websites/WebsiteMenuView.js +++ b/components/pages/websites/WebsiteMenuView.js @@ -1,7 +1,7 @@ -import { Row, Column, Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics'; +import { Menu, Item, Icon, Button, Flexbox, Text } from 'react-basics'; import { useIntl } from 'react-intl'; import Link from 'next/link'; -import classNames from 'classnames'; +import { GridRow, GridColumn } from 'components/layout/Grid'; import BrowsersTable from 'components/metrics/BrowsersTable'; import CountriesTable from 'components/metrics/CountriesTable'; import DevicesTable from 'components/metrics/DevicesTable'; @@ -33,7 +33,7 @@ const views = { export default function WebsiteMenuView({ websiteId, websiteDomain }) { const { formatMessage } = useIntl(); const { - resolve, + resolveUrl, query: { view }, } = usePageQuery(); @@ -80,12 +80,12 @@ export default function WebsiteMenuView({ websiteId, websiteDomain }) { }, ]; - const DetailsComponent = views[view]; + const DetailsComponent = views[view] || (() => null); return ( - - - + + +
- {!websiteId && websites.length > 1 && ( -
{getWebsite(row)?.domain}
- )}
); }; diff --git a/components/pages/realtime/RealtimeViews.js b/components/pages/realtime/RealtimeUrls.js similarity index 71% rename from components/pages/realtime/RealtimeViews.js rename to components/pages/realtime/RealtimeUrls.js index c1f93be1..8437af40 100644 --- a/components/pages/realtime/RealtimeViews.js +++ b/components/pages/realtime/RealtimeUrls.js @@ -1,5 +1,5 @@ -import { useMemo, useState, useCallback } from 'react'; -import { ButtonGroup, Button } from 'react-basics'; +import { useMemo, useState } from 'react'; +import { ButtonGroup, Button, Flexbox } from 'react-basics'; import { useIntl } from 'react-intl'; import firstBy from 'thenby'; import { percentFilter } from 'lib/filters'; @@ -7,18 +7,10 @@ import DataTable from 'components/metrics/DataTable'; import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants'; import { labels } from 'components/messages'; -export default function RealtimeViews({ websiteId, data = {}, websites }) { +export default function RealtimeUrls({ websiteDomain, data = {} }) { const { formatMessage } = useIntl(); const { pageviews } = data; const [filter, setFilter] = useState(FILTER_REFERRERS); - const domains = useMemo(() => websites.map(({ domain }) => domain), [websites]); - const getDomain = useCallback( - id => - websites.length === 1 - ? websites[0]?.domain - : websites.find(({ websiteId }) => websiteId === id)?.domain, - [websites], - ); const buttons = [ { @@ -32,7 +24,7 @@ export default function RealtimeViews({ websiteId, data = {}, websites }) { ]; const renderLink = ({ x }) => { - const domain = x.startsWith('/') ? getDomain(websiteId) : ''; + const domain = x.startsWith('/') ? websiteDomain : ''; return ( {x} @@ -48,7 +40,7 @@ export default function RealtimeViews({ websiteId, data = {}, websites }) { if (referrer?.startsWith('http')) { const hostname = new URL(referrer).hostname.replace(/^www\./, ''); - if (hostname && !domains.includes(hostname)) { + if (hostname) { const row = arr.find(({ x }) => x === hostname); if (!row) { @@ -65,11 +57,8 @@ export default function RealtimeViews({ websiteId, data = {}, websites }) { const pages = percentFilter( pageviews - .reduce((arr, { url, websiteId }) => { + .reduce((arr, { url }) => { if (url?.startsWith('/')) { - if (!websiteId && websites.length > 1) { - url = `${getDomain(websiteId)}${url}`; - } const row = arr.find(({ x }) => x === url); if (!row) { @@ -91,9 +80,11 @@ export default function RealtimeViews({ websiteId, data = {}, websites }) { return ( <> - - {({ key, label }) => } - + + + {({ key, label }) => } + + {filter === FILTER_REFERRERS && ( { - const url = new URL(window.location.href); + const url = new URL(window?.location?.href); const locale = url.searchParams.get('locale'); if (locale) { diff --git a/hooks/useTheme.js b/hooks/useTheme.js index 5c21bf1c..c50f442f 100644 --- a/hooks/useTheme.js +++ b/hooks/useTheme.js @@ -24,7 +24,7 @@ export default function useTheme() { }, [theme]); useEffect(() => { - const url = new URL(window.location.href); + const url = new URL(window?.location?.href); const theme = url.searchParams.get('theme'); if (['light', 'dark'].includes(theme)) { diff --git a/next.config.js b/next.config.js index eac47415..c654b086 100644 --- a/next.config.js +++ b/next.config.js @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ require('dotenv').config(); const pkg = require('./package.json'); diff --git a/pages/404.js b/pages/404.js index 9f13a545..12cb4113 100644 --- a/pages/404.js +++ b/pages/404.js @@ -1,18 +1,20 @@ +import { Row, Column, Flexbox } from 'react-basics'; +import { useIntl } from 'react-intl'; import AppLayout from 'components/layout/AppLayout'; -import { useIntl, defineMessages } from 'react-intl'; - -const messages = defineMessages({ - notFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' }, -}); +import { labels } from 'components/messages'; export default function Custom404() { const { formatMessage } = useIntl(); return ( -
-

{formatMessage(messages.notFound)}

-
+ + + +

{formatMessage(labels.pageNotFound)}

+
+
+
); } diff --git a/pages/api/realtime/[id].ts b/pages/api/realtime/[id].ts new file mode 100644 index 00000000..d8e6ad62 --- /dev/null +++ b/pages/api/realtime/[id].ts @@ -0,0 +1,20 @@ +import { subMinutes } from 'date-fns'; +import { RealtimeInit, NextApiRequestAuth } from 'lib/types'; +import { useAuth } from 'lib/middleware'; +import { NextApiResponse } from 'next'; +import { methodNotAllowed, ok } from 'next-basics'; +import { getRealtimeData } from 'queries'; + +export default async (req: NextApiRequestAuth, res: NextApiResponse) => { + await useAuth(req, res); + + if (req.method === 'GET') { + const { id } = req.query; + + const data = await getRealtimeData(id, subMinutes(new Date(), 30)); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/realtime/init.ts b/pages/api/realtime/init.ts deleted file mode 100644 index 1cb5746f..00000000 --- a/pages/api/realtime/init.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { subMinutes } from 'date-fns'; -import { RealtimeInit, NextApiRequestAuth } from 'lib/types'; -import { secret } from 'lib/crypto'; -import { useAuth } from 'lib/middleware'; -import { NextApiResponse } from 'next'; -import { createToken, methodNotAllowed, ok } from 'next-basics'; -import { getRealtimeData, getUserWebsites } from 'queries'; - -export default async (req: NextApiRequestAuth, res: NextApiResponse) => { - await useAuth(req, res); - - if (req.method === 'GET') { - const { id: userId } = req.auth.user; - - const websites = await getUserWebsites(userId); - const ids = websites.map(({ id }) => id); - const token = createToken({ websites: ids }, secret()); - const data = await getRealtimeData(ids, subMinutes(new Date(), 30)); - - return ok(res, { - websites, - token, - data, - }); - } - - return methodNotAllowed(res); -}; diff --git a/pages/api/realtime/update.ts b/pages/api/realtime/update.ts deleted file mode 100644 index 8125d02d..00000000 --- a/pages/api/realtime/update.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { ok, methodNotAllowed, badRequest, parseToken } from 'next-basics'; -import { useAuth } from 'lib/middleware'; -import { getRealtimeData } from 'queries'; -import { SHARE_TOKEN_HEADER } from 'lib/constants'; -import { secret } from 'lib/crypto'; -import { NextApiRequestQueryBody, RealtimeUpdate } from 'lib/types'; -import { NextApiResponse } from 'next'; - -export interface InitUpdateRequestQuery { - startAt: string; -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useAuth(req, res); - - if (req.method === 'GET') { - const { startAt } = req.query; - - const token = req.headers[SHARE_TOKEN_HEADER]; - - if (!token) { - return badRequest(res); - } - - const { websites } = parseToken(token, secret()); - - const data = await getRealtimeData(websites, new Date(+startAt)); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/pages/dashboard/[[...id]].js b/pages/dashboard/index.js similarity index 100% rename from pages/dashboard/[[...id]].js rename to pages/dashboard/index.js diff --git a/pages/realtime/[id]/index.js b/pages/realtime/[id]/index.js new file mode 100644 index 00000000..3ef556be --- /dev/null +++ b/pages/realtime/[id]/index.js @@ -0,0 +1,18 @@ +import { useRouter } from 'next/router'; +import AppLayout from 'components/layout/AppLayout'; +import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard'; + +export default function RealtimeDetailsPage() { + const router = useRouter(); + const { id: websiteId } = router.query; + + if (!websiteId) { + return null; + } + + return ( + + + + ); +} diff --git a/pages/realtime.js b/pages/realtime/index.js similarity index 58% rename from pages/realtime.js rename to pages/realtime/index.js index 008255e6..fa0d86d6 100644 --- a/pages/realtime.js +++ b/pages/realtime/index.js @@ -1,10 +1,10 @@ import AppLayout from 'components/layout/AppLayout'; -import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard'; +import RealtimeHome from 'components/pages/realtime/RealtimeHome'; export default function RealtimePage() { return ( - + ); } diff --git a/queries/analytics/event/getEvents.ts b/queries/analytics/event/getEvents.ts index 5d01824c..953b1a28 100644 --- a/queries/analytics/event/getEvents.ts +++ b/queries/analytics/event/getEvents.ts @@ -3,19 +3,17 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import { EVENT_TYPE } from 'lib/constants'; -export function getEvents(...args: [websites: string[], startAt: Date]) { +export function getEvents(...args: [websiteId: string, startAt: Date]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -function relationalQuery(websites: string[], startAt: Date) { - return prisma.client.event.findMany({ +function relationalQuery(websiteId: string, startAt: Date) { + return prisma.client.websiteEvent.findMany({ where: { - websiteId: { - in: websites, - }, + websiteId, createdAt: { gte: startAt, }, @@ -23,7 +21,7 @@ function relationalQuery(websites: string[], startAt: Date) { }); } -function clickhouseQuery(websites: string[], startAt: Date) { +function clickhouseQuery(websiteId: string, startAt: Date) { const { rawQuery } = clickhouse; return rawQuery( @@ -36,10 +34,10 @@ function clickhouseQuery(websites: string[], startAt: Date) { event_name from event where event_type = ${EVENT_TYPE.customEvent} - and ${websites && websites.length > 0 ? `website_id in {websites:Array(UUID)}` : '0 = 0'} + and website_id = {websiteId:UUID} and created_at >= {startAt:DateTime('UTC')}`, { - websites, + websiteId, startAt, }, ); diff --git a/queries/analytics/pageview/getPageviews.ts b/queries/analytics/pageview/getPageviews.ts index eb60a1f5..c37f23c1 100644 --- a/queries/analytics/pageview/getPageviews.ts +++ b/queries/analytics/pageview/getPageviews.ts @@ -3,19 +3,17 @@ import clickhouse from 'lib/clickhouse'; import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; import { EVENT_TYPE } from 'lib/constants'; -export async function getPageviews(...args: [websites: string[], startAt: Date]) { +export async function getPageviews(...args: [websiteId: string, startAt: Date]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websites: string[], startAt: Date) { - return prisma.client.pageview.findMany({ +async function relationalQuery(websiteId: string, startAt: Date) { + return prisma.client.websiteEvent.findMany({ where: { - websiteId: { - in: websites, - }, + websiteId, createdAt: { gte: startAt, }, @@ -23,21 +21,21 @@ async function relationalQuery(websites: string[], startAt: Date) { }); } -async function clickhouseQuery(websites: string[], startAt: Date) { +async function clickhouseQuery(websiteId: string, startAt: Date) { const { rawQuery } = clickhouse; return rawQuery( `select - website_id, - session_id, - created_at, + website_id as websiteId, + session_id as sessionId, + created_at as createdAt, url from event where event_type = ${EVENT_TYPE.pageView} - and ${websites && websites.length > 0 ? `website_id in {websites:Array(UUID)}` : '0 = 0'} + and website_id = {websiteId:UUID} and created_at >= {startAt:DateTime('UTC')}`, { - websites, + websiteId, startAt, }, ); diff --git a/queries/analytics/session/getSessions.ts b/queries/analytics/session/getSessions.ts index 46057277..1f28333b 100644 --- a/queries/analytics/session/getSessions.ts +++ b/queries/analytics/session/getSessions.ts @@ -2,23 +2,17 @@ import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; import { runQuery, PRISMA, CLICKHOUSE } from 'lib/db'; -export async function getSessions(...args: [websites: string[], startAt: Date]) { +export async function getSessions(...args: [websiteId: string, startAt: Date]) { return runQuery({ [PRISMA]: () => relationalQuery(...args), [CLICKHOUSE]: () => clickhouseQuery(...args), }); } -async function relationalQuery(websites: string[], startAt: Date) { +async function relationalQuery(websiteId: string, startAt: Date) { return prisma.client.session.findMany({ where: { - ...(websites && websites.length > 0 - ? { - websiteId: { - in: websites, - }, - } - : {}), + websiteId, createdAt: { gte: startAt, }, @@ -26,7 +20,7 @@ async function relationalQuery(websites: string[], startAt: Date) { }); } -async function clickhouseQuery(websites: string[], startAt: Date) { +async function clickhouseQuery(websiteId: string, startAt: Date) { const { rawQuery } = clickhouse; return rawQuery( @@ -42,10 +36,10 @@ async function clickhouseQuery(websites: string[], startAt: Date) { language, country from event - where ${websites && websites.length > 0 ? `website_id in {websites:Array(UUID)}` : '0 = 0'} + where website_id = {websiteId:UUID} and created_at >= {startAt:DateTime('UTC')}`, { - websites, + websiteId, startAt, }, ); diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts index 659f6145..46cb6746 100644 --- a/queries/analytics/stats/getRealtimeData.ts +++ b/queries/analytics/stats/getRealtimeData.ts @@ -2,11 +2,11 @@ import { getPageviews } from '../pageview/getPageviews'; import { getSessions } from '../session/getSessions'; import { getEvents } from '../event/getEvents'; -export async function getRealtimeData(websites, time) { +export async function getRealtimeData(websiteId, time) { const [pageviews, sessions, events] = await Promise.all([ - getPageviews(websites, time), - getSessions(websites, time), - getEvents(websites, time), + getPageviews(websiteId, time), + getSessions(websiteId, time), + getEvents(websiteId, time), ]); return { From 802c262cd973dc2342eb93cf9ce2617891df3ec8 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 17 Feb 2023 21:42:42 -0800 Subject: [PATCH 3/8] Fixed realtime chart display. --- components/metrics/RealtimeChart.js | 26 ++++----- .../pages/realtime/RealtimeCountries.js | 26 +++++++++ .../pages/realtime/RealtimeDashboard.js | 58 ++++++++----------- components/pages/realtime/RealtimeHeader.js | 9 ++- lib/constants.ts | 2 +- lib/crypto.js | 5 ++ pages/_app.js | 1 + pages/api/realtime/[id].ts | 9 ++- pages/realtime/[id]/index.js | 2 +- queries/analytics/event/getEvents.ts | 11 ++-- queries/analytics/pageview/getPageviews.ts | 1 + queries/analytics/session/getSessions.ts | 7 ++- queries/analytics/stats/getRealtimeData.ts | 28 +++++---- scripts/download-country-names.js | 2 +- 14 files changed, 112 insertions(+), 75 deletions(-) create mode 100644 components/pages/realtime/RealtimeCountries.js diff --git a/components/metrics/RealtimeChart.js b/components/metrics/RealtimeChart.js index f33126f0..56861e0d 100644 --- a/components/metrics/RealtimeChart.js +++ b/components/metrics/RealtimeChart.js @@ -1,5 +1,5 @@ import { useMemo, useRef } from 'react'; -import { format, parseISO, startOfMinute, subMinutes, isBefore } from 'date-fns'; +import { format, startOfMinute, subMinutes, isBefore } from 'date-fns'; import PageviewsChart from './PageviewsChart'; import { getDateArray } from 'lib/date'; import { DEFAULT_ANIMATION_DURATION, REALTIME_RANGE } from 'lib/constants'; @@ -8,13 +8,12 @@ function mapData(data) { let last = 0; const arr = []; - data.reduce((obj, val) => { - const { createdAt } = val; - const t = startOfMinute(parseISO(createdAt)); + data.reduce((obj, { timestamp }) => { + const t = startOfMinute(new Date(timestamp)); if (t.getTime() > last) { obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; arr.push(obj); - last = t; + last = t.getTime(); } else { obj.y += 1; } @@ -30,14 +29,15 @@ export default function RealtimeChart({ data, unit, ...props }) { const prevEndDate = useRef(endDate); const chartData = useMemo(() => { - if (data) { - return { - pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), - sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), - }; + if (!data) { + return { pageviews: [], sessions: [] }; } - return { pageviews: [], sessions: [] }; - }, [data]); + + return { + pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), + sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), + }; + }, [data, startDate, endDate, unit]); // Don't animate the bars shifting over because it looks weird const animationDuration = useMemo(() => { @@ -46,7 +46,7 @@ export default function RealtimeChart({ data, unit, ...props }) { return 0; } return DEFAULT_ANIMATION_DURATION; - }, [data]); + }, [data, endDate]); return ( {countryNames[x]}, + [countryNames, locale], + ); + + return ( + + ); +} diff --git a/components/pages/realtime/RealtimeDashboard.js b/components/pages/realtime/RealtimeDashboard.js index b45d85d2..a59a6d07 100644 --- a/components/pages/realtime/RealtimeDashboard.js +++ b/components/pages/realtime/RealtimeDashboard.js @@ -1,66 +1,57 @@ -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useIntl } from 'react-intl'; -import { subMinutes, startOfMinute, differenceInMinutes } from 'date-fns'; +import { subMinutes, startOfMinute } from 'date-fns'; +import { useRouter } from 'next/router'; import firstBy from 'thenby'; import { GridRow, GridColumn } from 'components/layout/Grid'; import Page from 'components/layout/Page'; import RealtimeChart from 'components/metrics/RealtimeChart'; -import RealtimeLog from 'components/pages/realtime/RealtimeLog'; -import RealtimeHeader from 'components/pages/realtime/RealtimeHeader'; import StickyHeader from 'components/helpers/StickyHeader'; import PageHeader from 'components/layout/PageHeader'; import WorldMap from 'components/common/WorldMap'; -import DataTable from 'components/metrics/DataTable'; +import RealtimeLog from 'components/pages/realtime/RealtimeLog'; +import RealtimeHeader from 'components/pages/realtime/RealtimeHeader'; import RealtimeUrls from 'components/pages/realtime/RealtimeUrls'; +import RealtimeCountries from 'components/pages/realtime/RealtimeCountries'; +import WebsiteSelect from 'components/input/WebsiteSelect'; import useApi from 'hooks/useApi'; -import useLocale from 'hooks/useLocale'; -import useCountryNames from 'hooks/useCountryNames'; import { percentFilter } from 'lib/filters'; import { labels } from 'components/messages'; import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; import styles from './RealtimeDashboard.module.css'; -import WebsiteSelect from '../../input/WebsiteSelect'; -import { useRouter } from 'next/router'; -function mergeData(state = [], data, time) { +function mergeData(state = [], data = [], time) { const ids = state.map(({ __id }) => __id); return state .concat(data.filter(({ __id }) => !ids.includes(__id))) - .filter(({ createdAt }) => new Date(createdAt).getTime() >= time); + .filter(({ timestamp }) => timestamp >= time); } export default function RealtimeDashboard({ websiteId }) { const { formatMessage } = useIntl(); - const { locale } = useLocale(); const router = useRouter(); - const countryNames = useCountryNames(locale); const [currentData, setCurrentData] = useState(); const { get, useQuery } = useApi(); const { data, isLoading, error } = useQuery( ['realtime', websiteId], - () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp }), + () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }), { enabled: !!websiteId, - retryInterval: REALTIME_INTERVAL, + refetchInterval: REALTIME_INTERVAL, + cache: false, }, ); - const renderCountryName = useCallback( - ({ x }) => {countryNames[x]}, - [countryNames], - ); - useEffect(() => { if (data) { - const { pageviews, sessions, events, timestamp } = data; - const time = subMinutes(startOfMinute(new Date()), REALTIME_RANGE).getTime(); + const date = subMinutes(startOfMinute(new Date()), REALTIME_RANGE); + const time = date.getTime(); setCurrentData(state => ({ - ...state, - pageviews: mergeData(state?.pageviews, pageviews, time), - sessions: mergeData(state?.sessions, sessions, time), - events: mergeData(state?.events, events, time), - timestamp, + pageviews: mergeData(state?.pageviews, data.pageviews, time), + sessions: mergeData(state?.sessions, data.sessions, time), + events: mergeData(state?.events, data.events, time), + timestamp: data.timestamp, })); } }, [data]); @@ -72,6 +63,12 @@ export default function RealtimeDashboard({ websiteId }) { currentData.countries = percentFilter( currentData.sessions + .reduce((arr, data) => { + if (!arr.find(({ sessionId }) => sessionId === data.sessionId)) { + return arr.concat(data); + } + return arr; + }, []) .reduce((arr, { country }) => { if (country) { const row = arr.find(({ x }) => x === country); @@ -115,12 +112,7 @@ export default function RealtimeDashboard({ websiteId }) { - + diff --git a/components/pages/realtime/RealtimeHeader.js b/components/pages/realtime/RealtimeHeader.js index 2f34e766..bfcce6e9 100644 --- a/components/pages/realtime/RealtimeHeader.js +++ b/components/pages/realtime/RealtimeHeader.js @@ -7,13 +7,20 @@ export default function RealtimeHeader({ data = {} }) { const { formatMessage } = useIntl(); const { pageviews, sessions, events, countries } = data; + const visitors = sessions?.reduce((arr, { sessionId }) => { + if (sessionId && !arr.includes(sessionId)) { + return arr.concat(sessionId); + } + return arr; + }, []); + return (
diff --git a/lib/constants.ts b/lib/constants.ts index 90f21388..f7bce52e 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -21,7 +21,7 @@ export const DEFAULT_DATE_RANGE = '24hour'; export const DEFAULT_WEBSITE_LIMIT = 10; export const REALTIME_RANGE = 30; -export const REALTIME_INTERVAL = 3000; +export const REALTIME_INTERVAL = 5000; export const UI_LAYOUT_BODY = 'ui-layout-body'; diff --git a/lib/crypto.js b/lib/crypto.js index ae609d0f..fcd3e2c0 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -1,3 +1,4 @@ +import crypto from 'crypto'; import { v4, v5 } from 'uuid'; import { startOfMonth } from 'date-fns'; import { hash } from 'next-basics'; @@ -17,3 +18,7 @@ export function uuid(...args) { return v5(hash(...args, salt()), v5.DNS); } + +export function md5(...args) { + return crypto.createHash('md5').update(args.join('')).digest('hex'); +} diff --git a/pages/_app.js b/pages/_app.js index 66dd6f73..3fe13b0c 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -14,6 +14,7 @@ import '@fontsource/inter/600.css'; const client = new QueryClient({ defaultOptions: { queries: { + retry: false, refetchOnWindowFocus: false, }, }, diff --git a/pages/api/realtime/[id].ts b/pages/api/realtime/[id].ts index d8e6ad62..069db54a 100644 --- a/pages/api/realtime/[id].ts +++ b/pages/api/realtime/[id].ts @@ -9,9 +9,14 @@ export default async (req: NextApiRequestAuth, res: NextApiResponse startTime.getTime()) { + startTime = new Date(+startAt); + } + + const data = await getRealtimeData(id, startTime); return ok(res, data); } diff --git a/pages/realtime/[id]/index.js b/pages/realtime/[id]/index.js index 3ef556be..1eabda32 100644 --- a/pages/realtime/[id]/index.js +++ b/pages/realtime/[id]/index.js @@ -12,7 +12,7 @@ export default function RealtimeDetailsPage() { return ( - + ); } diff --git a/queries/analytics/event/getEvents.ts b/queries/analytics/event/getEvents.ts index 953b1a28..d4a3d6ce 100644 --- a/queries/analytics/event/getEvents.ts +++ b/queries/analytics/event/getEvents.ts @@ -26,12 +26,13 @@ function clickhouseQuery(websiteId: string, startAt: Date) { return rawQuery( `select - event_id, - website_id, - session_id, - created_at, + event_id as id, + website_id as websiteId, + session_id as sessionId, + created_at as createdAt, + toUnixTimestamp(created_at) as timestamp, url, - event_name + event_name as eventName from event where event_type = ${EVENT_TYPE.customEvent} and website_id = {websiteId:UUID} diff --git a/queries/analytics/pageview/getPageviews.ts b/queries/analytics/pageview/getPageviews.ts index c37f23c1..33fa33db 100644 --- a/queries/analytics/pageview/getPageviews.ts +++ b/queries/analytics/pageview/getPageviews.ts @@ -29,6 +29,7 @@ async function clickhouseQuery(websiteId: string, startAt: Date) { website_id as websiteId, session_id as sessionId, created_at as createdAt, + toUnixTimestamp(created_at) as timestamp, url from event where event_type = ${EVENT_TYPE.pageView} diff --git a/queries/analytics/session/getSessions.ts b/queries/analytics/session/getSessions.ts index 1f28333b..e0769b37 100644 --- a/queries/analytics/session/getSessions.ts +++ b/queries/analytics/session/getSessions.ts @@ -25,9 +25,10 @@ async function clickhouseQuery(websiteId: string, startAt: Date) { return rawQuery( `select distinct - session_id, - website_id, - created_at, + session_id as sessionId, + website_id as websiteId, + created_at as createdAt, + toUnixTimestamp(created_at) as timestamp, hostname, browser, os, diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts index 46cb6746..052259fb 100644 --- a/queries/analytics/stats/getRealtimeData.ts +++ b/queries/analytics/stats/getRealtimeData.ts @@ -1,3 +1,4 @@ +import { md5 } from 'lib/crypto'; import { getPageviews } from '../pageview/getPageviews'; import { getSessions } from '../session/getSessions'; import { getEvents } from '../event/getEvents'; @@ -9,22 +10,19 @@ export async function getRealtimeData(websiteId, time) { getEvents(websiteId, time), ]); + const decorate = (id, data) => { + return data.map(props => ({ + ...props, + __id: md5(id, ...Object.values(props)), + timestamp: props.timestamp * 1000, + timestampCompare: new Date(props.createdAt).getTime(), + })); + }; + return { - pageviews: pageviews.map(({ id, ...props }) => ({ - __id: `p${id}`, - pageviewId: id, - ...props, - })), - sessions: sessions.map(({ id, ...props }) => ({ - __id: `s${id}`, - sessionId: id, - ...props, - })), - events: events.map(({ id, ...props }) => ({ - __id: `e${id}`, - eventId: id, - ...props, - })), + pageviews: decorate('pageviews', pageviews), + sessions: decorate('sessions', sessions), + events: decorate('events', events), timestamp: Date.now(), }; } diff --git a/scripts/download-country-names.js b/scripts/download-country-names.js index a180e7af..f56d91f9 100644 --- a/scripts/download-country-names.js +++ b/scripts/download-country-names.js @@ -1,4 +1,4 @@ -/* eslint-disable no-console */ +/* eslint-disable no-console, @typescript-eslint/no-var-requires */ const fs = require('fs-extra'); const path = require('path'); const https = require('https'); From 8ecc6400ef806b557954f057dac256769e025fd3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 17 Feb 2023 22:17:29 -0800 Subject: [PATCH 4/8] Fixed visitor calculation. --- components/metrics/RealtimeChart.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/components/metrics/RealtimeChart.js b/components/metrics/RealtimeChart.js index 56861e0d..6ead67bf 100644 --- a/components/metrics/RealtimeChart.js +++ b/components/metrics/RealtimeChart.js @@ -33,9 +33,16 @@ export default function RealtimeChart({ data, unit, ...props }) { return { pageviews: [], sessions: [] }; } + const visitors = data.sessions?.reduce((arr, val) => { + if (!arr.find(({ sessionId }) => sessionId === val.sessionId)) { + return arr.concat(val); + } + return arr; + }, []); + return { pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), - sessions: getDateArray(mapData(data.sessions), startDate, endDate, unit), + sessions: getDateArray(mapData(visitors), startDate, endDate, unit), }; }, [data, startDate, endDate, unit]); From 7d3334ccceffc59800acd1897847209092dcd0c3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Wed, 22 Feb 2023 20:59:59 -0800 Subject: [PATCH 5/8] Fixed realtime logs. --- components/messages.js | 9 ++ components/metrics/RealtimeChart.js | 11 +- .../pages/realtime/RealtimeDashboard.js | 9 +- components/pages/realtime/RealtimeHeader.js | 9 +- components/pages/realtime/RealtimeLog.js | 140 ++++++++---------- .../pages/realtime/RealtimeLog.module.css | 8 +- queries/analytics/stats/getRealtimeData.ts | 10 +- 7 files changed, 88 insertions(+), 108 deletions(-) diff --git a/components/messages.js b/components/messages.js index 28f5bf18..cc1d0e86 100644 --- a/components/messages.js +++ b/components/messages.js @@ -97,6 +97,7 @@ export const labels = defineMessages({ all: { id: 'label.all', defaultMessage: 'All' }, sessions: { id: 'label.sessions', defaultMessage: 'Sessions' }, pageNotFound: { id: 'message.page-not-found', defaultMessage: 'Page not found' }, + logs: { id: 'label.activity-log', defaultMessage: 'Activity log' }, }); export const messages = defineMessages({ @@ -168,6 +169,14 @@ export const messages = defineMessages({ id: 'message.team-not-found', defaultMessage: 'Team not found.', }, + visitorLog: { + id: 'message.visitor-log', + defaultMessage: 'Visitor from {country} using {browser} on {os} {device}', + }, + eventLog: { + id: 'message.event-log', + defaultMessage: '{event} on {url}', + }, }); export const devices = defineMessages({ diff --git a/components/metrics/RealtimeChart.js b/components/metrics/RealtimeChart.js index 6ead67bf..00bd0051 100644 --- a/components/metrics/RealtimeChart.js +++ b/components/metrics/RealtimeChart.js @@ -8,7 +8,7 @@ function mapData(data) { let last = 0; const arr = []; - data.reduce((obj, { timestamp }) => { + data?.reduce((obj, { timestamp }) => { const t = startOfMinute(new Date(timestamp)); if (t.getTime() > last) { obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 }; @@ -33,16 +33,9 @@ export default function RealtimeChart({ data, unit, ...props }) { return { pageviews: [], sessions: [] }; } - const visitors = data.sessions?.reduce((arr, val) => { - if (!arr.find(({ sessionId }) => sessionId === val.sessionId)) { - return arr.concat(val); - } - return arr; - }, []); - return { pageviews: getDateArray(mapData(data.pageviews), startDate, endDate, unit), - sessions: getDateArray(mapData(visitors), startDate, endDate, unit), + sessions: getDateArray(mapData(data.visitors), startDate, endDate, unit), }; }, [data, startDate, endDate, unit]); diff --git a/components/pages/realtime/RealtimeDashboard.js b/components/pages/realtime/RealtimeDashboard.js index a59a6d07..9ac6a863 100644 --- a/components/pages/realtime/RealtimeDashboard.js +++ b/components/pages/realtime/RealtimeDashboard.js @@ -58,7 +58,7 @@ export default function RealtimeDashboard({ websiteId }) { const realtimeData = useMemo(() => { if (!currentData) { - return { pageviews: [], sessions: [], events: [], countries: [] }; + return { pageviews: [], sessions: [], events: [], countries: [], visitors: [] }; } currentData.countries = percentFilter( @@ -84,6 +84,13 @@ export default function RealtimeDashboard({ websiteId }) { .sort(firstBy('y', -1)), ); + currentData.visitors = currentData.sessions.reduce((arr, val) => { + if (!arr.find(({ sessionId }) => sessionId === val.sessionId)) { + return arr.concat(val); + } + return arr; + }, []); + return currentData; }, [currentData]); diff --git a/components/pages/realtime/RealtimeHeader.js b/components/pages/realtime/RealtimeHeader.js index bfcce6e9..15730d5d 100644 --- a/components/pages/realtime/RealtimeHeader.js +++ b/components/pages/realtime/RealtimeHeader.js @@ -5,14 +5,7 @@ import styles from './RealtimeHeader.module.css'; export default function RealtimeHeader({ data = {} }) { const { formatMessage } = useIntl(); - const { pageviews, sessions, events, countries } = data; - - const visitors = sessions?.reduce((arr, { sessionId }) => { - if (sessionId && !arr.includes(sessionId)) { - return arr.concat(sessionId); - } - return arr; - }, []); + const { pageviews, visitors, events, countries } = data; return (
diff --git a/components/pages/realtime/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js index 25fba487..06b7537e 100644 --- a/components/pages/realtime/RealtimeLog.js +++ b/components/pages/realtime/RealtimeLog.js @@ -1,11 +1,11 @@ import { useMemo, useState } from 'react'; -import { StatusLight, Icon } from 'react-basics'; +import { StatusLight, Icon, Text } from 'react-basics'; import { useIntl, FormattedMessage } from 'react-intl'; import { FixedSizeList } from 'react-window'; import firstBy from 'thenby'; import FilterButtons from 'components/common/FilterButtons'; import NoData from 'components/common/NoData'; -import { getDeviceMessage, labels } from 'components/messages'; +import { getDeviceMessage, labels, messages } from 'components/messages'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; @@ -15,12 +15,12 @@ import { safeDecodeURI } from 'next-basics'; import Icons from 'components/icons'; import styles from './RealtimeLog.module.css'; -const TYPE_ALL = 'type-all'; -const TYPE_PAGEVIEW = 'type-pageview'; -const TYPE_SESSION = 'type-session'; -const TYPE_EVENT = 'type-event'; +const TYPE_ALL = 'all'; +const TYPE_PAGEVIEW = 'pageview'; +const TYPE_SESSION = 'session'; +const TYPE_EVENT = 'event'; -const TYPE_ICONS = { +const icons = { [TYPE_PAGEVIEW]: , [TYPE_SESSION]: , [TYPE_EVENT]: , @@ -32,30 +32,6 @@ export default function RealtimeLog({ data, websiteDomain }) { const countryNames = useCountryNames(locale); const [filter, setFilter] = useState(TYPE_ALL); - const logs = useMemo(() => { - if (!data) { - return []; - } - - const { pageviews, sessions, events } = data; - const logs = [...pageviews, ...sessions, ...events].sort(firstBy('createdAt', -1)); - if (filter) { - return logs.filter(row => getType(row) === filter); - } - return logs; - }, [data, filter]); - - const uuids = useMemo(() => { - if (!data) { - return []; - } - - return data.sessions.reduce((obj, { sessionId, sessionUuid }) => { - obj[sessionId] = sessionUuid; - return obj; - }, {}); - }, [data]); - const buttons = [ { label: formatMessage(labels.all), @@ -66,7 +42,7 @@ export default function RealtimeLog({ data, websiteDomain }) { key: TYPE_PAGEVIEW, }, { - label: formatMessage(labels.sessions), + label: formatMessage(labels.visitors), key: TYPE_SESSION, }, { @@ -75,42 +51,41 @@ export default function RealtimeLog({ data, websiteDomain }) { }, ]; - function getType({ pageviewId, sessionId, eventId }) { - if (eventId) { - return TYPE_EVENT; - } - if (pageviewId) { - return TYPE_PAGEVIEW; - } - if (sessionId) { - return TYPE_SESSION; - } - return null; - } + const getTime = ({ createdAt }) => dateFormat(new Date(createdAt), 'pp', locale); - function getIcon(row) { - return TYPE_ICONS[getType(row)]; - } + const getColor = ({ sessionId }) => stringToColor(sessionId); - function getDetail({ - eventName, - pageviewId, - sessionId, - url, - browser, - os, - country, - device, - websiteId, - }) { - if (eventName) { - return
{eventName}
; + const getIcon = ({ __type }) => icons[__type]; + + const getDetail = log => { + const { __type, eventName, url, browser, os, country, device } = log; + + if (__type === TYPE_EVENT) { + return ( + {eventName || formatMessage(labels.unknown)}, + url: ( +
+ {url} + + ), + }} + /> + ); } - if (pageviewId) { + + if (__type === TYPE_PAGEVIEW) { return ( @@ -118,11 +93,11 @@ export default function RealtimeLog({ data, websiteDomain }) { ); } - if (sessionId) { + + if (__type === TYPE_SESSION) { return ( {countryNames[country] || formatMessage(labels.unknown)}, browser: {BROWSERS[browser]}, @@ -132,17 +107,7 @@ export default function RealtimeLog({ data, websiteDomain }) { /> ); } - } - - function getTime({ createdAt }) { - return dateFormat(new Date(createdAt), 'pp', locale); - } - - function getColor(row) { - const { sessionId } = row; - - return stringToColor(uuids[sessionId] || `${sessionId}}`); - } + }; const Row = ({ index, style }) => { const row = logs[index]; @@ -153,19 +118,32 @@ export default function RealtimeLog({ data, websiteDomain }) {
{getTime(row)}
- - {getDetail(row)} + {getIcon(row)} + {getDetail(row)}
); }; + const logs = useMemo(() => { + if (!data) { + return []; + } + + const { pageviews, visitors, events } = data; + const logs = [...pageviews, ...visitors, ...events].sort(firstBy('createdAt', -1)); + + if (filter !== TYPE_ALL) { + return logs.filter(({ __type }) => __type === filter); + } + + return logs; + }, [data, filter]); + return (
-
- -
+
{formatMessage(labels.logs)}
{logs?.length === 0 && } {logs?.length > 0 && ( diff --git a/components/pages/realtime/RealtimeLog.module.css b/components/pages/realtime/RealtimeLog.module.css index 292bbd8c..7e54e34c 100644 --- a/components/pages/realtime/RealtimeLog.module.css +++ b/components/pages/realtime/RealtimeLog.module.css @@ -1,16 +1,14 @@ .table { - font-size: var(--font-size-xs); + font-size: var(--font-size-sm); overflow: hidden; height: 100%; - display: grid; - grid-template-rows: fit-content(100%) fit-content(100%) auto; } .header { display: flex; align-items: center; justify-content: space-between; - font-size: 16px; + font-size: var(--font-size-md); line-height: 40px; font-weight: 600; } @@ -18,6 +16,7 @@ .row { display: flex; align-items: center; + gap: 10px; height: 40px; border-bottom: 1px solid var(--base300); } @@ -44,6 +43,7 @@ .detail { display: flex; flex: 1; + gap: 10px; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; diff --git a/queries/analytics/stats/getRealtimeData.ts b/queries/analytics/stats/getRealtimeData.ts index 052259fb..989194bf 100644 --- a/queries/analytics/stats/getRealtimeData.ts +++ b/queries/analytics/stats/getRealtimeData.ts @@ -14,15 +14,15 @@ export async function getRealtimeData(websiteId, time) { return data.map(props => ({ ...props, __id: md5(id, ...Object.values(props)), - timestamp: props.timestamp * 1000, - timestampCompare: new Date(props.createdAt).getTime(), + __type: id, + timestamp: props.timestamp ? props.timestamp * 1000 : new Date(props.createdAt).getTime(), })); }; return { - pageviews: decorate('pageviews', pageviews), - sessions: decorate('sessions', sessions), - events: decorate('events', events), + pageviews: decorate('pageview', pageviews), + sessions: decorate('session', sessions), + events: decorate('event', events), timestamp: Date.now(), }; } From 5657a64c773755edd8cfb6574dd368a62a72f727 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Fri, 24 Feb 2023 02:41:02 -0800 Subject: [PATCH 6/8] Pass domain to realtime components. --- components/common/OverflowText.js | 60 ------------------- components/common/OverflowText.module.css | 6 -- components/input/WebsiteSelect.js | 2 +- components/metrics/WebsiteHeader.js | 5 +- .../pages/realtime/RealtimeDashboard.js | 7 ++- components/pages/realtime/RealtimeHome.js | 4 +- .../teams/{[...id].js => [id]/index.js} | 0 .../users/{[...id].js => [id]/index.js} | 0 8 files changed, 9 insertions(+), 75 deletions(-) delete mode 100644 components/common/OverflowText.js delete mode 100644 components/common/OverflowText.module.css rename pages/settings/teams/{[...id].js => [id]/index.js} (100%) rename pages/settings/users/{[...id].js => [id]/index.js} (100%) diff --git a/components/common/OverflowText.js b/components/common/OverflowText.js deleted file mode 100644 index aaeb9c46..00000000 --- a/components/common/OverflowText.js +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; -import ReactTooltip from 'react-tooltip'; - -import styles from './OverflowText.module.css'; - -const OverflowText = ({ children, tooltipId }) => { - const measureEl = useRef(); - const [isOverflown, setIsOverflown] = useState(false); - - const measure = useCallback( - el => { - if (!el) return; - setIsOverflown(el.scrollWidth > el.clientWidth); - }, - [setIsOverflown], - ); - - // Do one measure on mount - useEffect(() => { - measure(measureEl.current); - }, [measure]); - - // Set up resize listener for subsequent measures - useEffect(() => { - if (!measureEl.current) return; - - // Destructure ref in case it changes out from under us - const el = measureEl.current; - - if ('ResizeObserver' in global) { - // Ideally, we have access to ResizeObservers - const observer = new ResizeObserver(() => { - measure(el); - }); - observer.observe(el); - return () => observer.unobserve(el); - } else { - // Otherwise, fall back to measuring on window resizes - const handler = () => measure(el); - - window.addEventListener('resize', handler, { passive: true }); - return () => window.removeEventListener('resize', handler, { passive: true }); - } - }); - - return ( - - {children} - {isOverflown && {children}} - - ); -}; - -export default OverflowText; diff --git a/components/common/OverflowText.module.css b/components/common/OverflowText.module.css deleted file mode 100644 index c2066631..00000000 --- a/components/common/OverflowText.module.css +++ /dev/null @@ -1,6 +0,0 @@ -.root { - max-width: 100%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} diff --git a/components/input/WebsiteSelect.js b/components/input/WebsiteSelect.js index 82f516e8..2a1a4e17 100644 --- a/components/input/WebsiteSelect.js +++ b/components/input/WebsiteSelect.js @@ -22,7 +22,7 @@ export default function WebsiteSelect({ websiteId, onSelect }) { placeholder={formatMessage(labels.selectWebsite)} style={{ width: 200 }} > - {item => {item.name}} + {({ id, name }) => {name}} ); } diff --git a/components/metrics/WebsiteHeader.js b/components/metrics/WebsiteHeader.js index 8f102c08..d1817c8b 100644 --- a/components/metrics/WebsiteHeader.js +++ b/components/metrics/WebsiteHeader.js @@ -1,6 +1,5 @@ -import { Row, Column } from 'react-basics'; +import { Row, Column, Text } from 'react-basics'; import Favicon from 'components/common/Favicon'; -import OverflowText from 'components/common/OverflowText'; import ActiveUsers from './ActiveUsers'; import styles from './WebsiteHeader.module.css'; @@ -9,7 +8,7 @@ export default function WebsiteHeader({ websiteId, title, domain, children }) { - {title} + {title} diff --git a/components/pages/realtime/RealtimeDashboard.js b/components/pages/realtime/RealtimeDashboard.js index 9ac6a863..9e6c2d1b 100644 --- a/components/pages/realtime/RealtimeDashboard.js +++ b/components/pages/realtime/RealtimeDashboard.js @@ -32,11 +32,12 @@ export default function RealtimeDashboard({ websiteId }) { const router = useRouter(); const [currentData, setCurrentData] = useState(); const { get, useQuery } = useApi(); + const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`)); const { data, isLoading, error } = useQuery( ['realtime', websiteId], () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }), { - enabled: !!websiteId, + enabled: !!(websiteId && website), refetchInterval: REALTIME_INTERVAL, cache: false, }, @@ -111,10 +112,10 @@ export default function RealtimeDashboard({ websiteId }) {
- + - + diff --git a/components/pages/realtime/RealtimeHome.js b/components/pages/realtime/RealtimeHome.js index f54ed4de..67b34ee9 100644 --- a/components/pages/realtime/RealtimeHome.js +++ b/components/pages/realtime/RealtimeHome.js @@ -17,10 +17,10 @@ export default function RealtimeHome() { if (data?.length) { router.push(`realtime/${data[0].id}`); } - }, [data]); + }, [data, router]); return ( - + 0} error={error}> {data?.length === 0 && } diff --git a/pages/settings/teams/[...id].js b/pages/settings/teams/[id]/index.js similarity index 100% rename from pages/settings/teams/[...id].js rename to pages/settings/teams/[id]/index.js diff --git a/pages/settings/users/[...id].js b/pages/settings/users/[id]/index.js similarity index 100% rename from pages/settings/users/[...id].js rename to pages/settings/users/[id]/index.js From 84430e38eb0ad52c42e66ebe7b1b334b62cdd90f Mon Sep 17 00:00:00 2001 From: Brian Cao Date: Mon, 27 Feb 2023 16:01:34 -0800 Subject: [PATCH 7/8] Check deletedAt. (#1796) --- pages/api/users/index.ts | 2 +- queries/admin/teamWebsite.ts | 14 +++------- queries/admin/user.ts | 50 ++++++++++++++++++++++-------------- queries/admin/website.ts | 33 +++++++++--------------- 4 files changed, 47 insertions(+), 52 deletions(-) diff --git a/pages/api/users/index.ts b/pages/api/users/index.ts index 89d4d564..6146682e 100644 --- a/pages/api/users/index.ts +++ b/pages/api/users/index.ts @@ -36,7 +36,7 @@ export default async ( const { username, password, id } = req.body; - const existingUser = await getUser({ username }); + const existingUser = await getUser({ username }, { showDeleted: true }); if (existingUser) { return badRequest(res, 'User already exists'); diff --git a/queries/admin/teamWebsite.ts b/queries/admin/teamWebsite.ts index 1a2b3891..b2c7ee7b 100644 --- a/queries/admin/teamWebsite.ts +++ b/queries/admin/teamWebsite.ts @@ -3,7 +3,7 @@ import { uuid } from 'lib/crypto'; import prisma from 'lib/prisma'; export async function getTeamWebsite(teamId: string, userId: string): Promise { - return prisma.client.TeamWebsite.findFirst({ + return prisma.client.teamWebsite.findFirst({ where: { teamId, userId, @@ -12,7 +12,7 @@ export async function getTeamWebsite(teamId: string, userId: string): Promise { - return prisma.client.TeamWebsite.findMany({ + return prisma.client.teamWebsite.findMany({ where: { teamId, }, @@ -28,7 +28,7 @@ export async function createTeamWebsite( teamId: string, websiteId: string, ): Promise { - return prisma.client.TeamWebsite.create({ + return prisma.client.teamWebsite.create({ data: { id: uuid(), userId, @@ -37,11 +37,3 @@ export async function createTeamWebsite( }, }); } - -export async function deleteTeamWebsite(TeamWebsiteId: string): Promise { - return prisma.client.teamUser.delete({ - where: { - id: TeamWebsiteId, - }, - }); -} diff --git a/queries/admin/user.ts b/queries/admin/user.ts index ba6ef186..2b7677f9 100644 --- a/queries/admin/user.ts +++ b/queries/admin/user.ts @@ -11,13 +11,13 @@ export interface User { } export async function getUser( - where: Prisma.UserWhereUniqueInput, - options: { includePassword?: boolean } = {}, + where: Prisma.UserWhereInput | Prisma.UserWhereUniqueInput, + options: { includePassword?: boolean; showDeleted?: boolean } = {}, ): Promise { - const { includePassword = false } = options; + const { includePassword = false, showDeleted = false } = options; - return prisma.client.user.findUnique({ - where, + return prisma.client.user.findFirst({ + where: { ...where, ...(showDeleted ? {} : { deletedAt: null }) }, select: { id: true, username: true, @@ -69,6 +69,7 @@ export async function getUserWebsites(userId: string): Promise { return prisma.client.website.findMany({ where: { userId, + deletedAt: null, }, orderBy: [ { @@ -118,6 +119,7 @@ export async function deleteUser( userId: string, ): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Prisma.BatchPayload, User]> { const { client } = prisma; + const cloudMode = process.env.CLOUD_MODE; const websites = await client.website.findMany({ where: { userId }, @@ -137,20 +139,30 @@ export async function deleteUser( client.session.deleteMany({ where: { websiteId: { in: websiteIds } }, }), - client.website.updateMany({ - data: { - deletedAt: new Date(), - }, - where: { id: { in: websiteIds } }, - }), - client.user.update({ - data: { - deletedAt: new Date(), - }, - where: { - id: userId, - }, - }), + cloudMode + ? client.website.updateMany({ + data: { + deletedAt: new Date(), + }, + where: { id: { in: websiteIds } }, + }) + : client.website.deleteMany({ + where: { id: { in: websiteIds } }, + }), + cloudMode + ? client.user.update({ + data: { + deletedAt: new Date(), + }, + where: { + id: userId, + }, + }) + : client.user.delete({ + where: { + id: userId, + }, + }), ]) .then(async data => { if (cache.enabled) { diff --git a/queries/admin/website.ts b/queries/admin/website.ts index cf2425a3..a364b26f 100644 --- a/queries/admin/website.ts +++ b/queries/admin/website.ts @@ -1,7 +1,6 @@ import { Prisma, Website } from '@prisma/client'; import cache from 'lib/cache'; import prisma from 'lib/prisma'; -import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db'; export async function getWebsite(where: Prisma.WebsiteWhereUniqueInput): Promise { return prisma.client.website.findUnique({ @@ -69,17 +68,11 @@ export async function resetWebsite( }); } -export async function deleteWebsite(websiteId: string) { - return runQuery({ - [PRISMA]: () => deleteWebsiteRelationalQuery(websiteId), - [CLICKHOUSE]: () => deleteWebsiteClickhouseQuery(websiteId), - }); -} - -async function deleteWebsiteRelationalQuery( +export async function deleteWebsite( websiteId, ): Promise<[Prisma.BatchPayload, Prisma.BatchPayload, Website]> { const { client, transaction } = prisma; + const cloudMode = process.env.CLOUD_MODE; return transaction([ client.websiteEvent.deleteMany({ @@ -88,9 +81,16 @@ async function deleteWebsiteRelationalQuery( client.session.deleteMany({ where: { websiteId }, }), - client.website.delete({ - where: { id: websiteId }, - }), + cloudMode + ? prisma.client.website.update({ + data: { + deletedAt: new Date(), + }, + where: { id: websiteId }, + }) + : client.website.delete({ + where: { id: websiteId }, + }), ]).then(async data => { if (cache.enabled) { await cache.deleteWebsite(websiteId); @@ -99,12 +99,3 @@ async function deleteWebsiteRelationalQuery( return data; }); } - -async function deleteWebsiteClickhouseQuery(websiteId): Promise { - return prisma.client.website.update({ - data: { - deletedAt: new Date(), - }, - where: { id: websiteId }, - }); -} From 3ac560dc0fd2518b12497ed02a4baf7de442c0c2 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 27 Feb 2023 20:03:04 -0800 Subject: [PATCH 8/8] Added checks for CLOUD_MODE. --- components/layout/AppLayout.js | 4 ++- components/layout/NavBar.js | 16 +++++++++-- .../pages/settings/profile/ProfileSettings.js | 4 ++- components/pages/settings/teams/TeamsList.js | 2 +- .../{WebsiteReset.js => WebsiteData.js} | 28 +++++++++++-------- .../settings/websites/WebsiteSettings.js | 8 +++--- lib/types.ts | 7 +++++ package.json | 2 +- pages/api/auth/login.ts | 4 +-- pages/api/config.ts | 2 ++ pages/api/me/index.ts | 13 +++++++++ pages/api/me/password.ts | 9 ++++-- pages/console/[[...id]].js | 6 ++-- pages/login.js | 6 ++-- pages/logout.js | 22 +++++++++++---- pages/settings/index.js | 4 ++- pages/settings/teams/[id]/index.js | 14 +++++++++- pages/settings/teams/index.js | 14 +++++++++- pages/settings/users/[id]/index.js | 14 +++++++++- pages/settings/users/index.js | 16 +++++++---- pages/settings/websites/[id]/index.js | 12 ++++++-- pages/settings/websites/index.js | 14 +++++++++- queries/admin/user.ts | 9 +----- styles/index.css | 5 ++++ 24 files changed, 175 insertions(+), 60 deletions(-) rename components/pages/settings/websites/{WebsiteReset.js => WebsiteData.js} (61%) create mode 100644 pages/api/me/index.ts diff --git a/components/layout/AppLayout.js b/components/layout/AppLayout.js index d89c34b9..ad744fab 100644 --- a/components/layout/AppLayout.js +++ b/components/layout/AppLayout.js @@ -2,13 +2,15 @@ import { Container } from 'react-basics'; import Head from 'next/head'; import NavBar from 'components/layout/NavBar'; import useRequireLogin from 'hooks/useRequireLogin'; +import useConfig from 'hooks/useConfig'; import { UI_LAYOUT_BODY } from 'lib/constants'; import styles from './AppLayout.module.css'; export default function AppLayout({ title, children }) { const { user } = useRequireLogin(); + const config = useConfig(); - if (!user) { + if (!user || !config) { return null; } diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js index 07a72bc1..80307568 100644 --- a/components/layout/NavBar.js +++ b/components/layout/NavBar.js @@ -10,9 +10,11 @@ import { labels } from 'components/messages'; import useUser from 'hooks/useUser'; import NavGroup from './NavGroup'; import styles from './NavBar.module.css'; +import useConfig from 'hooks/useConfig'; export default function NavBar() { const { user } = useUser(); + const { cloudMode } = useConfig(); const { formatMessage } = useIntl(); const [minimized, setMinimized] = useState(false); const tooltipPosition = minimized ? 'right' : 'top'; @@ -24,13 +26,21 @@ export default function NavBar() { ]; const settings = [ - { label: formatMessage(labels.websites), url: '/settings/websites', icon: }, + !cloudMode && { + label: formatMessage(labels.websites), + url: '/settings/websites', + icon: , + }, user?.isAdmin && { label: formatMessage(labels.users), url: '/settings/users', icon: , }, - { label: formatMessage(labels.teams), url: '/settings/teams', icon: }, + !cloudMode && { + label: formatMessage(labels.teams), + url: '/settings/teams', + icon: , + }, { label: formatMessage(labels.profile), url: '/settings/profile', icon: }, ].filter(n => n); @@ -53,7 +63,7 @@ export default function NavBar() {
- + {!cloudMode && }
diff --git a/components/pages/settings/profile/ProfileSettings.js b/components/pages/settings/profile/ProfileSettings.js index 9d22cfae..d302b6b2 100644 --- a/components/pages/settings/profile/ProfileSettings.js +++ b/components/pages/settings/profile/ProfileSettings.js @@ -4,14 +4,16 @@ import PageHeader from 'components/layout/PageHeader'; import ProfileDetails from './ProfileDetails'; import PasswordChangeButton from './PasswordChangeButton'; import { labels } from 'components/messages'; +import useConfig from 'hooks/useConfig'; export default function ProfileSettings() { const { formatMessage } = useIntl(); + const { cloudMode } = useConfig(); return ( - + {!cloudMode && } diff --git a/components/pages/settings/teams/TeamsList.js b/components/pages/settings/teams/TeamsList.js index a2ed628c..b48e9971 100644 --- a/components/pages/settings/teams/TeamsList.js +++ b/components/pages/settings/teams/TeamsList.js @@ -65,7 +65,7 @@ export default function TeamsList() { return ( {toast} - + {hasData && ( {joinButton} diff --git a/components/pages/settings/websites/WebsiteReset.js b/components/pages/settings/websites/WebsiteData.js similarity index 61% rename from components/pages/settings/websites/WebsiteReset.js rename to components/pages/settings/websites/WebsiteData.js index d742d113..64979fe4 100644 --- a/components/pages/settings/websites/WebsiteReset.js +++ b/components/pages/settings/websites/WebsiteData.js @@ -1,10 +1,10 @@ -import { Button, Form, FormRow, Modal, ModalTrigger } from 'react-basics'; +import { Button, Modal, ModalTrigger, ActionForm } from 'react-basics'; import { useIntl } from 'react-intl'; import WebsiteDeleteForm from 'components/pages/settings/websites/WebsiteDeleteForm'; import WebsiteResetForm from 'components/pages/settings/websites/WebsiteResetForm'; import { labels, messages } from 'components/messages'; -export default function WebsiteReset({ websiteId, onSave }) { +export default function WebsiteData({ websiteId, onSave }) { const { formatMessage } = useIntl(); const handleReset = async () => { @@ -16,29 +16,33 @@ export default function WebsiteReset({ websiteId, onSave }) { }; return ( -
- -

{formatMessage(messages.resetWebsiteWarning)}

+ <> + - + {close => ( )} -
- -

{formatMessage(messages.deleteWebsiteWarning)}

+ + - + {close => ( )} -
-
+ + ); } diff --git a/components/pages/settings/websites/WebsiteSettings.js b/components/pages/settings/websites/WebsiteSettings.js index e61c699d..02693c72 100644 --- a/components/pages/settings/websites/WebsiteSettings.js +++ b/components/pages/settings/websites/WebsiteSettings.js @@ -6,7 +6,7 @@ import Link from 'next/link'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import WebsiteEditForm from 'components/pages/settings/websites/WebsiteEditForm'; -import WebsiteReset from 'components/pages/settings/websites/WebsiteReset'; +import WebsiteData from 'components/pages/settings/websites/WebsiteData'; import TrackingCode from 'components/pages/settings/websites/TrackingCode'; import ShareUrl from 'components/pages/settings/websites/ShareUrl'; import useApi from 'hooks/useApi'; @@ -59,8 +59,8 @@ export default function WebsiteSettings({ websiteId }) { } > - - + +