From c865f43b111850185aba893fb47fbb8110b3eec3 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 10 Jul 2023 04:35:19 -0700 Subject: [PATCH] Metrics components refactoring. New event data page. --- assets/overview.svg | 2 +- components/common/HamburgerButton.js | 1 - components/input/WebsiteDateFilter.js | 13 +- components/input/WebsiteDateFilter.module.css | 3 + components/layout/NavBar.js | 2 - components/metrics/MetricsBar.js | 107 +------------ components/metrics/MetricsBar.module.css | 1 + components/metrics/WebsiteChart.js | 132 ---------------- components/metrics/WebsiteHeader.js | 42 ------ components/pages/console/TestConsole.js | 9 +- .../pages/event-data/EventDataMetricsBar.js | 47 ++++++ .../event-data/EventDataMetricsBar.module.css | 42 ++++++ components/pages/event-data/EventDataTable.js | 18 +++ .../{RealtimeDashboard.js => RealtimePage.js} | 24 +-- ...ard.module.css => RealtimePage.module.css} | 0 .../{ReportsList.js => ReportsPage.js} | 6 +- components/pages/websites/WebsiteChart.js | 59 ++++++++ .../pages/websites/WebsiteChart.module.css | 17 +++ components/pages/websites/WebsiteChartList.js | 33 ++-- components/pages/websites/WebsiteDetails.js | 47 ------ .../pages/websites/WebsiteDetails.module.css | 31 ---- .../pages/websites/WebsiteDetailsPage.js | 37 +++++ components/pages/websites/WebsiteEventData.js | 35 +++++ .../websites/WebsiteEventData.module.css | 9 ++ .../pages/websites/WebsiteEventDataPage.js | 12 ++ components/pages/websites/WebsiteHeader.js | 78 ++++++++++ .../websites}/WebsiteHeader.module.css | 4 + .../pages/websites/WebsiteMetricsBar.js | 138 +++++++++++++++++ .../websites/WebsiteMetricsBar.module.css} | 30 +--- .../pages/websites/WebsiteReportsPage.js | 30 ++++ hooks/index.js | 1 + hooks/useReports.js | 4 +- hooks/useWebsite.js | 10 ++ package.json | 2 +- pages/api/event-data/fields.ts | 36 +++++ pages/api/event-data/index.ts | 37 +++++ pages/api/reports/event-data.ts | 78 ---------- pages/api/reports/index.ts | 11 +- pages/api/websites/[id]/eventData.ts | 60 -------- pages/reports/index.js | 13 -- pages/websites/[id]/event-data.js | 4 +- pages/websites/[id]/realtime.js | 4 +- pages/websites/[id]/reports.js | 4 +- public/images/os/mac-os.png | Bin 624 -> 4736 bytes queries/admin/report.ts | 6 +- queries/analytics/eventData/getEventData.ts | 141 ++++++++---------- yarn.lock | 8 +- 47 files changed, 756 insertions(+), 672 deletions(-) create mode 100644 components/input/WebsiteDateFilter.module.css delete mode 100644 components/metrics/WebsiteChart.js delete mode 100644 components/metrics/WebsiteHeader.js create mode 100644 components/pages/event-data/EventDataMetricsBar.js create mode 100644 components/pages/event-data/EventDataMetricsBar.module.css create mode 100644 components/pages/event-data/EventDataTable.js rename components/pages/realtime/{RealtimeDashboard.js => RealtimePage.js} (83%) rename components/pages/realtime/{RealtimeDashboard.module.css => RealtimePage.module.css} (100%) rename components/pages/reports/{ReportsList.js => ReportsPage.js} (86%) create mode 100644 components/pages/websites/WebsiteChart.js create mode 100644 components/pages/websites/WebsiteChart.module.css delete mode 100644 components/pages/websites/WebsiteDetails.js delete mode 100644 components/pages/websites/WebsiteDetails.module.css create mode 100644 components/pages/websites/WebsiteDetailsPage.js create mode 100644 components/pages/websites/WebsiteEventData.js create mode 100644 components/pages/websites/WebsiteEventData.module.css create mode 100644 components/pages/websites/WebsiteEventDataPage.js create mode 100644 components/pages/websites/WebsiteHeader.js rename components/{metrics => pages/websites}/WebsiteHeader.module.css (91%) create mode 100644 components/pages/websites/WebsiteMetricsBar.js rename components/{metrics/WebsiteChart.module.css => pages/websites/WebsiteMetricsBar.module.css} (67%) create mode 100644 components/pages/websites/WebsiteReportsPage.js create mode 100644 hooks/useWebsite.js create mode 100644 pages/api/event-data/fields.ts create mode 100644 pages/api/event-data/index.ts delete mode 100644 pages/api/reports/event-data.ts delete mode 100644 pages/api/websites/[id]/eventData.ts delete mode 100644 pages/reports/index.js diff --git a/assets/overview.svg b/assets/overview.svg index 4c96e23d..ec44b4ea 100644 --- a/assets/overview.svg +++ b/assets/overview.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/components/common/HamburgerButton.js b/components/common/HamburgerButton.js index f4b12859..48c80770 100644 --- a/components/common/HamburgerButton.js +++ b/components/common/HamburgerButton.js @@ -15,7 +15,6 @@ export function HamburgerButton() { label: formatMessage(labels.dashboard), url: '/dashboard', }, - { label: formatMessage(labels.realtime), url: '/realtime' }, !cloudMode && { label: formatMessage(labels.settings), url: '/settings', diff --git a/components/input/WebsiteDateFilter.js b/components/input/WebsiteDateFilter.js index 71075dd7..88f4bd85 100644 --- a/components/input/WebsiteDateFilter.js +++ b/components/input/WebsiteDateFilter.js @@ -1,11 +1,12 @@ import useApi from 'hooks/useApi'; import useDateRange from 'hooks/useDateRange'; import DateFilter from './DateFilter'; +import styles from './WebsiteDateFilter.module.css'; -export default function WebsiteDateFilter({ websiteId, value }) { +export default function WebsiteDateFilter({ websiteId }) { const { get } = useApi(); const [dateRange, setDateRange] = useDateRange(websiteId); - const { startDate, endDate } = dateRange; + const { value, startDate, endDate } = dateRange; const handleChange = async value => { if (value === 'all' && websiteId) { @@ -20,6 +21,12 @@ export default function WebsiteDateFilter({ websiteId, value }) { }; return ( - + ); } diff --git a/components/input/WebsiteDateFilter.module.css b/components/input/WebsiteDateFilter.module.css new file mode 100644 index 00000000..13234c55 --- /dev/null +++ b/components/input/WebsiteDateFilter.module.css @@ -0,0 +1,3 @@ +.dropdown { + min-width: 200px; +} diff --git a/components/layout/NavBar.js b/components/layout/NavBar.js index a5ac35ef..97eaa46c 100644 --- a/components/layout/NavBar.js +++ b/components/layout/NavBar.js @@ -18,8 +18,6 @@ export function NavBar() { const links = [ { label: formatMessage(labels.dashboard), url: '/dashboard' }, - { label: formatMessage(labels.realtime), url: '/realtime' }, - { label: formatMessage(labels.reports), url: '/reports' }, !cloudMode && { label: formatMessage(labels.settings), url: '/settings' }, ].filter(n => n); diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js index ccaf627c..88f9488a 100644 --- a/components/metrics/MetricsBar.js +++ b/components/metrics/MetricsBar.js @@ -1,114 +1,13 @@ -import { useState } from 'react'; import { Loading } from 'react-basics'; import ErrorMessage from 'components/common/ErrorMessage'; -import useApi from 'hooks/useApi'; -import useDateRange from 'hooks/useDateRange'; -import usePageQuery from 'hooks/usePageQuery'; -import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format'; -import MetricCard from './MetricCard'; -import useMessages from 'hooks/useMessages'; import styles from './MetricsBar.module.css'; -export function MetricsBar({ websiteId }) { - const { formatMessage, labels } = useMessages(); - const { get, useQuery } = useApi(); - const [dateRange] = useDateRange(websiteId); - const { startDate, endDate, modified } = dateRange; - const [format, setFormat] = useState(true); - const { - query: { url, referrer, title, os, browser, device, country, region, city }, - } = usePageQuery(); - - const { data, error, isLoading, isFetched } = useQuery( - [ - 'websites:stats', - { websiteId, modified, url, referrer, title, os, browser, device, country, region, city }, - ], - () => - get(`/websites/${websiteId}/stats`, { - startAt: +startDate, - endAt: +endDate, - url, - referrer, - title, - os, - browser, - device, - country, - region, - city, - }), - ); - - const formatFunc = format - ? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`) - : formatNumber; - - function handleSetFormat() { - setFormat(state => !state); - } - - const { pageviews, uniques, bounces, totaltime } = data || {}; - const num = Math.min(data && uniques.value, data && bounces.value); - const diffs = data && { - pageviews: pageviews.value - pageviews.change, - uniques: uniques.value - uniques.change, - bounces: bounces.value - bounces.change, - totaltime: totaltime.value - totaltime.change, - }; - +export function MetricsBar({ children, onClick, isLoading, isFetched, error }) { return ( -
+
{isLoading && !isFetched && } {error && } - {data && !error && isFetched && ( - <> - - - Number(n).toFixed(0) + '%'} - reverseColors - /> - `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}`} - /> - - )} + {children}
); } diff --git a/components/metrics/MetricsBar.module.css b/components/metrics/MetricsBar.module.css index eaf81c48..eb33a324 100644 --- a/components/metrics/MetricsBar.module.css +++ b/components/metrics/MetricsBar.module.css @@ -1,5 +1,6 @@ .bar { display: flex; + flex-direction: row; cursor: pointer; min-height: 110px; gap: 20px; diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js deleted file mode 100644 index 7b902df1..00000000 --- a/components/metrics/WebsiteChart.js +++ /dev/null @@ -1,132 +0,0 @@ -import { useMemo } from 'react'; -import { Button, Icon, Text, Row, Column } from 'react-basics'; -import Link from 'next/link'; -import classNames from 'classnames'; -import PageviewsChart from './PageviewsChart'; -import MetricsBar from './MetricsBar'; -import WebsiteHeader from './WebsiteHeader'; -import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; -import ErrorMessage from 'components/common/ErrorMessage'; -import FilterTags from 'components/metrics/FilterTags'; -import RefreshButton from 'components/input/RefreshButton'; -import useApi from 'hooks/useApi'; -import useDateRange from 'hooks/useDateRange'; -import useTimezone from 'hooks/useTimezone'; -import usePageQuery from 'hooks/usePageQuery'; -import { getDateArray, getDateLength } from 'lib/date'; -import Icons from 'components/icons'; -import useSticky from 'hooks/useSticky'; -import useMessages from 'hooks/useMessages'; -import styles from './WebsiteChart.module.css'; -import useLocale from 'hooks/useLocale'; - -export function WebsiteChart({ - websiteId, - name, - domain, - stickyHeader = false, - showChart = true, - showDetailsButton = false, - onDataLoad = () => {}, -}) { - const { formatMessage, labels } = useMessages(); - const [dateRange] = useDateRange(websiteId); - const { startDate, endDate, unit, value, modified } = dateRange; - const [timezone] = useTimezone(); - const { - query: { url, referrer, os, browser, device, country, region, city, title }, - } = usePageQuery(); - const { get, useQuery } = useApi(); - const { ref, isSticky } = useSticky({ enabled: stickyHeader }); - - const { data, isLoading, error } = useQuery( - [ - 'websites:pageviews', - { websiteId, modified, url, referrer, os, browser, device, country, region, city, title }, - ], - () => - get(`/websites/${websiteId}/pageviews`, { - startAt: +startDate, - endAt: +endDate, - unit, - timezone, - url, - referrer, - os, - browser, - device, - country, - region, - city, - title, - }), - { onSuccess: onDataLoad }, - ); - - const chartData = useMemo(() => { - if (data) { - return { - pageviews: getDateArray(data.pageviews, startDate, endDate, unit), - sessions: getDateArray(data.sessions, startDate, endDate, unit), - }; - } - return { pageviews: [], sessions: [] }; - }, [data, modified]); - - const { dir } = useLocale(); - return ( - <> - - {showDetailsButton && ( - - - - )} - - - - - - - -
- - -
-
-
- - - {error && } - {showChart && ( - - )} - - - - ); -} - -export default WebsiteChart; diff --git a/components/metrics/WebsiteHeader.js b/components/metrics/WebsiteHeader.js deleted file mode 100644 index 9159a315..00000000 --- a/components/metrics/WebsiteHeader.js +++ /dev/null @@ -1,42 +0,0 @@ -import { Flexbox, Row, Column, Text, Button, Icon } from 'react-basics'; -import Favicon from 'components/common/Favicon'; -import ActiveUsers from './ActiveUsers'; -import styles from './WebsiteHeader.module.css'; -import { useMessages } from 'hooks'; -import Icons from 'components/icons'; - -export function WebsiteHeader({ websiteId, name, domain, children }) { - const { formatMessage, labels } = useMessages(); - - const links = [ - { label: formatMessage(labels.overview), icon: }, - { label: formatMessage(labels.realtime), icon: }, - { label: formatMessage(labels.reports), icon: }, - { label: formatMessage(labels.eventData), icon: }, - ]; - - return ( - - - - {name} - - - - - {links.map(({ label, icon }) => { - return ( - - ); - })} - - {children} - - - ); -} - -export default WebsiteHeader; diff --git a/components/pages/console/TestConsole.js b/components/pages/console/TestConsole.js index eda93f0b..3e907856 100644 --- a/components/pages/console/TestConsole.js +++ b/components/pages/console/TestConsole.js @@ -2,7 +2,7 @@ import WebsiteSelect from 'components/input/WebsiteSelect'; import Page from 'components/layout/Page'; import PageHeader from 'components/layout/PageHeader'; import EventsChart from 'components/metrics/EventsChart'; -import WebsiteChart from 'components/metrics/WebsiteChart'; +import WebsiteChart from 'components/pages/websites/WebsiteChart'; import useApi from 'hooks/useApi'; import Head from 'next/head'; import Link from 'next/link'; @@ -143,12 +143,7 @@ export function TestConsole() { - + diff --git a/components/pages/event-data/EventDataMetricsBar.js b/components/pages/event-data/EventDataMetricsBar.js new file mode 100644 index 00000000..649e934a --- /dev/null +++ b/components/pages/event-data/EventDataMetricsBar.js @@ -0,0 +1,47 @@ +import { Column, Row } from 'react-basics'; +import { useApi, useDateRange } from 'hooks'; +import MetricCard from 'components/metrics/MetricCard'; +import useMessages from 'hooks/useMessages'; +import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; +import MetricsBar from 'components/metrics/MetricsBar'; +import styles from './EventDataMetricsBar.module.css'; + +export function EventDataMetricsBar({ websiteId }) { + const { formatMessage, labels } = useMessages(); + const { get, useQuery } = useApi(); + const [dateRange] = useDateRange(websiteId); + const { startDate, endDate, modified } = dateRange; + + const { data, error, isLoading, isFetched } = useQuery( + ['event-data:fields', { websiteId, startDate, endDate, modified }], + () => + get(`/event-data/fields`, { + websiteId, + startAt: +startDate, + endAt: +endDate, + }), + ); + + return ( + + + + {!error && isFetched && ( + + )} + + + +
+ +
+
+
+ ); +} + +export default EventDataMetricsBar; diff --git a/components/pages/event-data/EventDataMetricsBar.module.css b/components/pages/event-data/EventDataMetricsBar.module.css new file mode 100644 index 00000000..739fe324 --- /dev/null +++ b/components/pages/event-data/EventDataMetricsBar.module.css @@ -0,0 +1,42 @@ +.container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 0; + min-height: 90px; + margin-bottom: 20px; + background: var(--base50); + z-index: var(--z-index-above); +} + +.metrics { + display: flex; + flex-direction: row; + align-items: center; +} + +.actions { + display: flex; + flex-direction: row; + align-items: center; + justify-content: flex-end; + flex: 1; +} + +.bar { + display: flex; + cursor: pointer; + min-height: 110px; + gap: 20px; + flex-wrap: wrap; +} + +.card { + justify-self: flex-start; +} + +@media only screen and (max-width: 992px) { + .card { + flex-basis: calc(50% - 20px); + } +} diff --git a/components/pages/event-data/EventDataTable.js b/components/pages/event-data/EventDataTable.js new file mode 100644 index 00000000..7c0ae1d8 --- /dev/null +++ b/components/pages/event-data/EventDataTable.js @@ -0,0 +1,18 @@ +import { GridTable, GridColumn } from 'react-basics'; +import { useMessages } from 'hooks'; + +export function EventDataTable({ data = [], showValue }) { + const { formatMessage, labels } = useMessages(); + + return ( + + + + ); +} + +export default EventDataTable; diff --git a/components/pages/realtime/RealtimeDashboard.js b/components/pages/realtime/RealtimePage.js similarity index 83% rename from components/pages/realtime/RealtimeDashboard.js rename to components/pages/realtime/RealtimePage.js index a1b862cb..2d2eceba 100644 --- a/components/pages/realtime/RealtimeDashboard.js +++ b/components/pages/realtime/RealtimePage.js @@ -1,22 +1,20 @@ import { useState, useEffect, useMemo } from 'react'; 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 PageHeader from 'components/layout/PageHeader'; import WorldMap from 'components/common/WorldMap'; 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 WebsiteHeader from 'components/pages/websites/WebsiteHeader'; import useApi from 'hooks/useApi'; -import useMessages from 'hooks/useMessages'; import { percentFilter } from 'lib/filters'; import { REALTIME_RANGE, REALTIME_INTERVAL } from 'lib/constants'; -import styles from './RealtimeDashboard.module.css'; +import styles from './RealtimePage.module.css'; +import { useWebsite } from 'hooks'; function mergeData(state = [], data = [], time) { const ids = state.map(({ __id }) => __id); @@ -25,12 +23,10 @@ function mergeData(state = [], data = [], time) { .filter(({ timestamp }) => timestamp >= time); } -export function RealtimeDashboard({ websiteId }) { - const { formatMessage, labels } = useMessages(); - const router = useRouter(); +export function RealtimePage({ websiteId }) { const [currentData, setCurrentData] = useState(); const { get, useQuery } = useApi(); - const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`)); + const { data: website } = useWebsite(websiteId); const { data, isLoading, error } = useQuery( ['realtime', websiteId], () => get(`/realtime/${websiteId}`, { startAt: currentData?.timestamp || 0 }), @@ -93,15 +89,9 @@ export function RealtimeDashboard({ websiteId }) { return currentData; }, [currentData]); - const handleSelect = id => { - router.push(`/realtime/${id}`); - }; - return ( - - - +
@@ -126,4 +116,4 @@ export function RealtimeDashboard({ websiteId }) { ); } -export default RealtimeDashboard; +export default RealtimePage; diff --git a/components/pages/realtime/RealtimeDashboard.module.css b/components/pages/realtime/RealtimePage.module.css similarity index 100% rename from components/pages/realtime/RealtimeDashboard.module.css rename to components/pages/realtime/RealtimePage.module.css diff --git a/components/pages/reports/ReportsList.js b/components/pages/reports/ReportsPage.js similarity index 86% rename from components/pages/reports/ReportsList.js rename to components/pages/reports/ReportsPage.js index 255cb546..470e1b08 100644 --- a/components/pages/reports/ReportsList.js +++ b/components/pages/reports/ReportsPage.js @@ -5,13 +5,13 @@ import { Button, Icon, Icons, Text } from 'react-basics'; import { useMessages, useReports } from 'hooks'; import ReportsTable from './ReportsTable'; -export function ReportsList() { +export function ReportsPage() { const { formatMessage, labels } = useMessages(); const { reports, error, isLoading } = useReports(); return ( - + + + + +
) : null; })} diff --git a/components/pages/websites/WebsiteDetails.js b/components/pages/websites/WebsiteDetails.js deleted file mode 100644 index ba80bcf8..00000000 --- a/components/pages/websites/WebsiteDetails.js +++ /dev/null @@ -1,47 +0,0 @@ -import { useState } from 'react'; -import { Loading } from 'react-basics'; -import Page from 'components/layout/Page'; -import WebsiteChart from 'components/metrics/WebsiteChart'; -import useApi from 'hooks/useApi'; -import usePageQuery from 'hooks/usePageQuery'; -import { DEFAULT_ANIMATION_DURATION } from 'lib/constants'; -import WebsiteTableView from './WebsiteTableView'; -import WebsiteMenuView from './WebsiteMenuView'; - -export default function WebsiteDetails({ websiteId }) { - const { get, useQuery } = useApi(); - const { data, isLoading, error } = useQuery(['websites', websiteId], () => - get(`/websites/${websiteId}`), - ); - const [chartLoaded, setChartLoaded] = useState(false); - - const { - query: { view }, - } = usePageQuery(); - - function handleDataLoad() { - if (!chartLoaded) { - setTimeout(() => setChartLoaded(true), DEFAULT_ANIMATION_DURATION); - } - } - - return ( - - - {!chartLoaded && } - {chartLoaded && ( - <> - {!view && } - {view && } - - )} - - ); -} diff --git a/components/pages/websites/WebsiteDetails.module.css b/components/pages/websites/WebsiteDetails.module.css deleted file mode 100644 index b0632be6..00000000 --- a/components/pages/websites/WebsiteDetails.module.css +++ /dev/null @@ -1,31 +0,0 @@ -.chart { - margin-bottom: 30px; -} - -.view { - border-top: 1px solid var(--base300); -} - -.menu { - font-size: var(--font-size-sm); -} - -.content { - min-height: 600px; - padding: 20px 0; -} - -.backButton { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 16px; -} - -.backButton svg { - transform: rotate(180deg); -} - -.hidden { - display: none; -} diff --git a/components/pages/websites/WebsiteDetailsPage.js b/components/pages/websites/WebsiteDetailsPage.js new file mode 100644 index 00000000..e6545ae2 --- /dev/null +++ b/components/pages/websites/WebsiteDetailsPage.js @@ -0,0 +1,37 @@ +import { Loading } from 'react-basics'; +import Page from 'components/layout/Page'; +import WebsiteChart from 'components/pages/websites/WebsiteChart'; +import FilterTags from 'components/metrics/FilterTags'; +import usePageQuery from 'hooks/usePageQuery'; +import WebsiteTableView from './WebsiteTableView'; +import WebsiteMenuView from './WebsiteMenuView'; +import { useWebsite } from 'hooks'; +import WebsiteHeader from './WebsiteHeader'; +import { WebsiteMetricsBar } from './WebsiteMetricsBar'; + +export default function WebsiteDetailsPage({ websiteId }) { + const { data: website, isLoading, error } = useWebsite(websiteId); + + const { + query: { view, url, referrer, os, browser, device, country, region, city, title }, + } = usePageQuery(); + + return ( + + + + + + {!website && } + {website && ( + <> + {!view && } + {view && } + + )} + + ); +} diff --git a/components/pages/websites/WebsiteEventData.js b/components/pages/websites/WebsiteEventData.js new file mode 100644 index 00000000..105995f9 --- /dev/null +++ b/components/pages/websites/WebsiteEventData.js @@ -0,0 +1,35 @@ +import EventDataTable from 'components/pages/event-data/EventDataTable'; +import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar'; +import { useDateRange, useApi, usePageQuery } from 'hooks'; +import styles from './WebsiteEventData.module.css'; + +function useFields(websiteId, field) { + const [dateRange] = useDateRange(websiteId); + const { startDate, endDate } = dateRange; + const { get, useQuery } = useApi(); + const { data, error, isLoading } = useQuery( + ['event-data:fields', websiteId, startDate, endDate], + () => + get('/event-data', { + websiteId, + startAt: +startDate, + endAt: +endDate, + field, + }), + { enabled: !!(websiteId && startDate && endDate) }, + ); + + return { data, error, isLoading }; +} + +export default function WebsiteEventData({ websiteId }) { + const { data } = useFields(websiteId); + const { query } = usePageQuery(); + + return ( +
+ + +
+ ); +} diff --git a/components/pages/websites/WebsiteEventData.module.css b/components/pages/websites/WebsiteEventData.module.css new file mode 100644 index 00000000..e835da71 --- /dev/null +++ b/components/pages/websites/WebsiteEventData.module.css @@ -0,0 +1,9 @@ +.container { + display: flex; + flex-direction: column; +} + +.header { + display: flex; + justify-content: flex-end; +} diff --git a/components/pages/websites/WebsiteEventDataPage.js b/components/pages/websites/WebsiteEventDataPage.js new file mode 100644 index 00000000..08acafb5 --- /dev/null +++ b/components/pages/websites/WebsiteEventDataPage.js @@ -0,0 +1,12 @@ +import Page from 'components/layout/Page'; +import WebsiteHeader from './WebsiteHeader'; +import WebsiteEventData from './WebsiteEventData'; + +export default function WebsiteEventDataPage({ websiteId }) { + return ( + + + + + ); +} diff --git a/components/pages/websites/WebsiteHeader.js b/components/pages/websites/WebsiteHeader.js new file mode 100644 index 00000000..8802c320 --- /dev/null +++ b/components/pages/websites/WebsiteHeader.js @@ -0,0 +1,78 @@ +import classNames from 'classnames'; +import { Flexbox, Row, Column, Text, Button, Icon } from 'react-basics'; +import Link from 'next/link'; +import { useRouter } from 'next/router'; +import Favicon from 'components/common/Favicon'; +import ActiveUsers from 'components/metrics/ActiveUsers'; +import styles from './WebsiteHeader.module.css'; +import Icons from 'components/icons'; +import { useMessages, useWebsite } from 'hooks'; + +export function WebsiteHeader({ websiteId, showLinks = true, children }) { + const { formatMessage, labels } = useMessages(); + const { asPath, pathname } = useRouter(); + const { data: website } = useWebsite(websiteId); + const { name, domain } = website || {}; + + const links = [ + { + label: formatMessage(labels.overview), + icon: , + path: '', + }, + { + label: formatMessage(labels.realtime), + icon: , + path: '/realtime', + }, + { + label: formatMessage(labels.reports), + icon: , + path: '/reports', + }, + { + label: formatMessage(labels.eventData), + icon: , + path: '/event-data', + }, + ]; + + return ( + + + + {name} + + + + {showLinks && ( + + {links.map(({ label, icon, path }) => { + const query = path.indexOf('?'); + const selected = path + ? asPath.endsWith(query >= 0 ? path.substring(0, query) : path) + : pathname === '/websites/[id]'; + + return ( + + + + ); + })} + + )} + {children} + + + ); +} + +export default WebsiteHeader; diff --git a/components/metrics/WebsiteHeader.module.css b/components/pages/websites/WebsiteHeader.module.css similarity index 91% rename from components/metrics/WebsiteHeader.module.css rename to components/pages/websites/WebsiteHeader.module.css index 408da0ec..89f78e52 100644 --- a/components/metrics/WebsiteHeader.module.css +++ b/components/pages/websites/WebsiteHeader.module.css @@ -23,3 +23,7 @@ gap: 30px; min-height: 0; } + +.selected { + font-weight: bold; +} diff --git a/components/pages/websites/WebsiteMetricsBar.js b/components/pages/websites/WebsiteMetricsBar.js new file mode 100644 index 00000000..9114e8f4 --- /dev/null +++ b/components/pages/websites/WebsiteMetricsBar.js @@ -0,0 +1,138 @@ +import { useState } from 'react'; +import classNames from 'classnames'; +import { Row, Column } from 'react-basics'; +import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format'; +import MetricCard from 'components/metrics/MetricCard'; +import RefreshButton from 'components/input/RefreshButton'; +import WebsiteDateFilter from 'components/input/WebsiteDateFilter'; +import MetricsBar from 'components/metrics/MetricsBar'; +import { useApi, useDateRange, usePageQuery, useMessages, useSticky } from 'hooks'; +import styles from './WebsiteMetricsBar.module.css'; + +export function WebsiteMetricsBar({ websiteId, sticky }) { + const { formatMessage, labels } = useMessages(); + const { get, useQuery } = useApi(); + const [dateRange] = useDateRange(websiteId); + const { startDate, endDate, modified } = dateRange; + const [format, setFormat] = useState(true); + const { ref, isSticky } = useSticky({ enabled: sticky }); + const { + query: { url, referrer, title, os, browser, device, country, region, city }, + } = usePageQuery(); + + const { data, error, isLoading, isFetched } = useQuery( + [ + 'websites:stats', + { websiteId, modified, url, referrer, title, os, browser, device, country, region, city }, + ], + () => + get(`/websites/${websiteId}/stats`, { + startAt: +startDate, + endAt: +endDate, + url, + referrer, + title, + os, + browser, + device, + country, + region, + city, + }), + ); + + const formatFunc = format + ? n => (n >= 0 ? formatLongNumber(n) : `-${formatLongNumber(Math.abs(n))}`) + : formatNumber; + + function handleSetFormat() { + setFormat(state => !state); + } + + const { pageviews, uniques, bounces, totaltime } = data || {}; + const num = Math.min(data && uniques.value, data && bounces.value); + const diffs = data && { + pageviews: pageviews.value - pageviews.change, + uniques: uniques.value - uniques.change, + bounces: bounces.value - bounces.change, + totaltime: totaltime.value - totaltime.change, + }; + + return ( + + + + {!error && isFetched && ( + <> + + + Number(n).toFixed(0) + '%'} + reverseColors + /> + + `${n < 0 ? '-' : ''}${formatShortTime(Math.abs(~~n), ['m', 's'], ' ')}` + } + /> + + )} + + + +
+ + +
+
+
+ ); +} + +export default WebsiteMetricsBar; diff --git a/components/metrics/WebsiteChart.module.css b/components/pages/websites/WebsiteMetricsBar.module.css similarity index 67% rename from components/metrics/WebsiteChart.module.css rename to components/pages/websites/WebsiteMetricsBar.module.css index c9334a27..52decfc6 100644 --- a/components/metrics/WebsiteChart.module.css +++ b/components/pages/websites/WebsiteMetricsBar.module.css @@ -1,22 +1,4 @@ .container { - position: relative; - display: flex; - flex-direction: column; - align-self: stretch; -} - -.chart { - position: relative; - overflow: hidden; -} - -.title { - font-size: var(--font-size-lg); - line-height: 60px; - font-weight: 600; -} - -.header { display: flex; justify-content: space-between; align-items: center; @@ -35,8 +17,10 @@ gap: 10px; } -.dropdown { - min-width: 200px; +@media only screen and (max-width: 1200px) { + .actions { + margin-top: 40px; + } } @media only screen and (min-width: 992px) { @@ -49,9 +33,3 @@ border-bottom: 1px solid var(--base300); } } - -@media only screen and (max-width: 1200px) { - .actions { - margin-top: 40px; - } -} diff --git a/components/pages/websites/WebsiteReportsPage.js b/components/pages/websites/WebsiteReportsPage.js new file mode 100644 index 00000000..b6f41bac --- /dev/null +++ b/components/pages/websites/WebsiteReportsPage.js @@ -0,0 +1,30 @@ +import Page from 'components/layout/Page'; +import Link from 'next/link'; +import { Button, Icon, Icons, Text, Flexbox } from 'react-basics'; +import { useMessages, useReports } from 'hooks'; +import ReportsTable from 'components/pages/reports/ReportsTable'; +import WebsiteHeader from './WebsiteHeader'; + +export function WebsiteReportsPage({ websiteId }) { + const { formatMessage, labels } = useMessages(); + const { reports, error, isLoading } = useReports(websiteId); + + return ( + + + + + + + + + + ); +} + +export default WebsiteReportsPage; diff --git a/hooks/index.js b/hooks/index.js index 892d52e4..6a9b3b35 100644 --- a/hooks/index.js +++ b/hooks/index.js @@ -18,3 +18,4 @@ export * from './useSticky'; export * from './useTheme'; export * from './useTimezone'; export * from './useUser'; +export * from './useWebsite'; diff --git a/hooks/useReports.js b/hooks/useReports.js index 0b5e60d0..90aa5cf5 100644 --- a/hooks/useReports.js +++ b/hooks/useReports.js @@ -1,8 +1,8 @@ import useApi from './useApi'; -export function useReports() { +export function useReports(websiteId) { const { get, useQuery } = useApi(); - const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`)); + const { data, error, isLoading } = useQuery(['reports'], () => get(`/reports`, { websiteId })); return { reports: data, error, isLoading }; } diff --git a/hooks/useWebsite.js b/hooks/useWebsite.js new file mode 100644 index 00000000..5315f0dc --- /dev/null +++ b/hooks/useWebsite.js @@ -0,0 +1,10 @@ +import useApi from './useApi'; + +export function useWebsite(websiteId) { + const { get, useQuery } = useApi(); + return useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), { + enabled: !!websiteId, + }); +} + +export default useWebsite; diff --git a/package.json b/package.json index 6d5ed0fe..c965e77c 100644 --- a/package.json +++ b/package.json @@ -95,7 +95,7 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.89.0", + "react-basics": "^0.91.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.4", diff --git a/pages/api/event-data/fields.ts b/pages/api/event-data/fields.ts new file mode 100644 index 00000000..f94d6c54 --- /dev/null +++ b/pages/api/event-data/fields.ts @@ -0,0 +1,36 @@ +import { canViewWebsite } from 'lib/auth'; +import { useCors, useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { getEventDataFields } from 'queries'; + +export interface EventDataFieldsRequestBody { + websiteId: string; + dateRange: { + startDate: string; + endDate: string; + }; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'GET') { + const { websiteId, startAt, endAt } = req.query; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt)); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/event-data/index.ts b/pages/api/event-data/index.ts new file mode 100644 index 00000000..d683156f --- /dev/null +++ b/pages/api/event-data/index.ts @@ -0,0 +1,37 @@ +import { canViewWebsite } from 'lib/auth'; +import { useCors, useAuth } from 'lib/middleware'; +import { NextApiRequestQueryBody } from 'lib/types'; +import { NextApiResponse } from 'next'; +import { ok, methodNotAllowed, unauthorized } from 'next-basics'; +import { getEventData } from 'queries'; + +export interface EventDataRequestBody { + websiteId: string; + dateRange: { + startDate: string; + endDate: string; + }; + field?: string; +} + +export default async ( + req: NextApiRequestQueryBody, + res: NextApiResponse, +) => { + await useCors(req, res); + await useAuth(req, res); + + if (req.method === 'GET') { + const { websiteId, startAt, endAt, field } = req.query; + + if (!(await canViewWebsite(req.auth, websiteId))) { + return unauthorized(res); + } + + const data = await getEventData(websiteId, new Date(+startAt), new Date(+endAt), field); + + return ok(res, data); + } + + return methodNotAllowed(res); +}; diff --git a/pages/api/reports/event-data.ts b/pages/api/reports/event-data.ts deleted file mode 100644 index e9135f81..00000000 --- a/pages/api/reports/event-data.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useCors, useAuth } from 'lib/middleware'; -import { NextApiRequestQueryBody } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { ok, methodNotAllowed, unauthorized } from 'next-basics'; -import { getEventDataFields } from 'queries/analytics/eventData/getEventDataFields'; -import { getEventData } from 'queries'; - -export interface EventDataRequestBody { - websiteId: string; - dateRange: { - startDate: string; - endDate: string; - }; - fields: [ - { - name: string; - type: string; - value: string; - }, - ]; - filters: [ - { - name: string; - type: string; - value: string; - }, - ]; - groups: [ - { - name: string; - type: string; - }, - ]; -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - - if (req.method === 'GET') { - const { websiteId, startAt, endAt } = req.query; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getEventDataFields(websiteId, new Date(+startAt), new Date(+endAt)); - - return ok(res, data); - } - - if (req.method === 'POST') { - const { - websiteId, - dateRange: { startDate, endDate }, - ...criteria - } = req.body; - - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const data = await getEventData( - websiteId, - new Date(startDate), - new Date(endDate), - criteria as any, - ); - - return ok(res, data); - } - - return methodNotAllowed(res); -}; diff --git a/pages/api/reports/index.ts b/pages/api/reports/index.ts index 55dc4bf5..b2c5da9e 100644 --- a/pages/api/reports/index.ts +++ b/pages/api/reports/index.ts @@ -2,8 +2,9 @@ import { uuid } from 'lib/crypto'; import { useAuth, useCors } from 'lib/middleware'; import { NextApiRequestQueryBody } from 'lib/types'; import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok } from 'next-basics'; +import { methodNotAllowed, ok, unauthorized } from 'next-basics'; import { createReport, getReports } from 'queries'; +import { canViewWebsite } from 'lib/auth'; export interface ReportRequestBody { websiteId: string; @@ -23,12 +24,18 @@ export default async ( await useCors(req, res); await useAuth(req, res); + const { websiteId } = req.query; + const { user: { id: userId }, } = req.auth; if (req.method === 'GET') { - const data = await getReports(userId); + if (!(websiteId && (await canViewWebsite(req.auth, websiteId)))) { + return unauthorized(res); + } + + const data = await getReports({ websiteId }); return ok(res, data); } diff --git a/pages/api/websites/[id]/eventData.ts b/pages/api/websites/[id]/eventData.ts deleted file mode 100644 index 04a6d83b..00000000 --- a/pages/api/websites/[id]/eventData.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { canViewWebsite } from 'lib/auth'; -import { useAuth, useCors } from 'lib/middleware'; -import { NextApiRequestQueryBody, WebsiteEventDataMetric } from 'lib/types'; -import { NextApiResponse } from 'next'; -import { methodNotAllowed, ok, unauthorized } from 'next-basics'; -import { getEventData } from 'queries'; - -export interface WebsiteEventDataRequestQuery { - id: string; -} - -export interface WebsiteEventDataRequestBody { - startAt: string; - endAt: string; - eventName?: string; - urlPath?: string; - timeSeries?: { - unit: string; - timezone: string; - }; - filters: [ - { - eventKey?: string; - eventValue?: string | number | boolean | Date; - }, - ]; -} - -export default async ( - req: NextApiRequestQueryBody, - res: NextApiResponse, -) => { - await useCors(req, res); - await useAuth(req, res); - - const { id: websiteId } = req.query; - - if (req.method === 'GET') { - if (!(await canViewWebsite(req.auth, websiteId))) { - return unauthorized(res); - } - - const { startAt, endAt, eventName, urlPath, filters } = req.body; - - const startDate = new Date(+startAt); - const endDate = new Date(+endAt); - - const events = await getEventData(websiteId, { - startDate, - endDate, - eventName, - urlPath, - filters, - }); - - return ok(res, events); - } - - return methodNotAllowed(res); -}; diff --git a/pages/reports/index.js b/pages/reports/index.js deleted file mode 100644 index e74bc05f..00000000 --- a/pages/reports/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import AppLayout from 'components/layout/AppLayout'; -import useMessages from 'hooks/useMessages'; -import ReportsPage from 'components/pages/reports/ReportsPage'; - -export default function () { - const { formatMessage, labels } = useMessages(); - - return ( - - - - ); -} diff --git a/pages/websites/[id]/event-data.js b/pages/websites/[id]/event-data.js index 7b060d20..8b44616d 100644 --- a/pages/websites/[id]/event-data.js +++ b/pages/websites/[id]/event-data.js @@ -1,6 +1,6 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; -import WebsiteEventData from 'components/pages/websites/WebsiteEventData'; +import WebsiteEventDataPage from 'components/pages/websites/WebsiteEventDataPage'; import useMessages from 'hooks/useMessages'; export default function () { @@ -14,7 +14,7 @@ export default function () { return ( - + ); } diff --git a/pages/websites/[id]/realtime.js b/pages/websites/[id]/realtime.js index ceab7ad2..efe486a5 100644 --- a/pages/websites/[id]/realtime.js +++ b/pages/websites/[id]/realtime.js @@ -1,6 +1,6 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; -import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard'; +import RealtimePage from 'components/pages/realtime/RealtimePage'; export default function () { const router = useRouter(); @@ -12,7 +12,7 @@ export default function () { return ( - + ); } diff --git a/pages/websites/[id]/reports.js b/pages/websites/[id]/reports.js index 2035b539..ccd88081 100644 --- a/pages/websites/[id]/reports.js +++ b/pages/websites/[id]/reports.js @@ -1,6 +1,6 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; -import WebsiteReports from 'components/pages/websites/WebsiteReports'; +import WebsiteReportsPage from 'components/pages/websites/WebsiteReportsPage'; export default function () { const router = useRouter(); @@ -12,7 +12,7 @@ export default function () { return ( - + ); } diff --git a/public/images/os/mac-os.png b/public/images/os/mac-os.png index 357e3eac4337f4ad57ae6fe5b18aff041587c391..1972abe7559bdc1478b02a35c0df20ed13824482 100644 GIT binary patch literal 4736 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 delta 598 zcmV-c0;&CgCGZ50C4X~5NmK|32nc)#WQYI&010qNS#tmY0Nnrp0Nnv_Q=$g|000?u zMObuGZ)S9NVRB^vcXxL#X>MzCV_|S*E^l&Yo9;Xs0005)Nkl3I)p&iE;{8RoW$|Z7O=GO-at97zd+GTd#$pvGyIX*= z_!At8i0Rb?NPjEArj&MRXF7=Icp4Euin*g0#v@EcL@T8(nkHv~$-Plizt_Xnf9t)Rk31>S36#KT25nuuX zbpY2gir-gJAIjou@ja9HS8Q9U5RGC$o^dxKzEw?>0e^-viPQf4m7@&MpAq2m{|0C^ z0a%w2V5AAaOh$lX9cN0v;ePpQKno+acLDk^pAq7PidwL|5>93W2uxwP4&Yp7fY82> z#ybJ5$EVByix{fx2_xb+F3US^M8uo2=<=_y0bl+hvnDo=!6qHig-ie!o3>;L_j)Wo zp{^gOfKS+nmpu}n#pcFMX$fK{=4t|e!~?vp@p+5g9X09z3W!a(Qi#0AZS6LE$1=aI kIF75@_sSfuV@qB9KPj(7MU~(|{Qv*}07*qoM6N<$g4=Bf8~^|S diff --git a/queries/admin/report.ts b/queries/admin/report.ts index 506902f5..47fe4eb4 100644 --- a/queries/admin/report.ts +++ b/queries/admin/report.ts @@ -13,11 +13,9 @@ export async function getReportById(reportId: string): Promise { }); } -export async function getReports(userId: string): Promise { +export async function getReports(where: Prisma.ReportWhereUniqueInput): Promise { return prisma.client.report.findMany({ - where: { - userId, - }, + where, }); } diff --git a/queries/analytics/eventData/getEventData.ts b/queries/analytics/eventData/getEventData.ts index 2f3b04eb..2f8b6992 100644 --- a/queries/analytics/eventData/getEventData.ts +++ b/queries/analytics/eventData/getEventData.ts @@ -3,26 +3,10 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db'; import { WebsiteEventDataMetric } from 'lib/types'; import { loadWebsite } from 'lib/query'; import { DEFAULT_CREATED_AT } from 'lib/constants'; - -export interface EventDataCriteria { - fields: [{ name: string; type: string; value: string }]; - filters: [ - { - name: string; - type: string; - value: [string, string]; - }, - ]; - groups: [ - { - name: string; - type: string; - }, - ]; -} +import prisma from '../../../lib/prisma'; export async function getEventData( - ...args: [websiteId: string, startDate: Date, endDate: Date, criteria: EventDataCriteria] + ...args: [websiteId: string, startDate: Date, endDate: Date, field?: string] ): Promise { return runQuery({ [PRISMA]: () => relationalQuery(...args), @@ -30,76 +14,79 @@ export async function getEventData( }); } -async function relationalQuery() { - return null; +async function relationalQuery(websiteId: string, startDate: Date, endDate: Date, field: string) { + const { toUuid, rawQuery } = prisma; + const website = await loadWebsite(websiteId); + const resetDate = new Date(website?.resetAt || DEFAULT_CREATED_AT); + + if (field) { + return rawQuery( + `select event_key as field, + count(*) as total + from event_data + where website_id = $1${toUuid()} + and event_key = $2 + and created_at >= $3 + and created_at between $4 and $5 + group by event_key + order by 2 desc, 1 asc + limit 1000 + `, + [websiteId, field, resetDate, startDate, endDate] as any, + ); + } + + return rawQuery( + `select + event_key as field, + count(*) as total + from event_data + where website_id = $1${toUuid()} + and created_at >= $2 + and created_at between $3 and $4 + group by event_key + order by 2 desc, 1 asc + limit 1000 + `, + [websiteId, resetDate, startDate, endDate] as any, + ); } -async function clickhouseQuery( - websiteId: string, - startDate: Date, - endDate: Date, - criteria: EventDataCriteria, -) { - const { fields, filters } = criteria; +async function clickhouseQuery(websiteId: string, startDate: Date, endDate: Date, field: string) { const { rawQuery, getDateFormat, getBetweenDates } = clickhouse; const website = await loadWebsite(websiteId); const resetDate = new Date(website?.resetAt || DEFAULT_CREATED_AT); - const uniqueFields = fields.reduce((obj, { name, type, value }) => { - const prefix = type === 'array' ? 'string' : type; - - if (!obj[name]) { - obj[name] = { - columns: [ - 'event_key as field', - `count(*) as total`, - value === 'unique' ? `${prefix}_value as value` : null, - ].filter(n => n), - groups: ['event_key', value === 'unique' ? `${prefix}_value` : null].filter(n => n), - }; - } - return obj; - }, {}); - - const queries = Object.keys(uniqueFields).reduce((arr, key) => { - const field = uniqueFields[key]; - const params = { websiteId, name: key }; - - return arr.concat( - rawQuery( - `select - ${field.columns.join(',')} + if (field) { + return rawQuery( + `select + event_key as field, + count(*) as total from event_data where website_id = {websiteId:UUID} - and event_key = {name:String} + and event_key = {field:String} and created_at >= ${getDateFormat(resetDate)} and ${getBetweenDates('created_at', startDate, endDate)} - group by ${field.groups.join(',')} - limit 100 + group by event_key + order by 2 desc, 1 asc + limit 1000 `, - params, - ), + { websiteId, field }, ); - }, []); + } - const results = (await Promise.all(queries)).flatMap(n => n); - - const columns = results.reduce((arr, row) => { - const keys = Object.keys(row); - for (const key of keys) { - if (!arr.includes(key)) { - arr.push(key); - } - } - return arr; - }, []); - - return results.reduce((arr, row) => { - return arr.concat( - columns.reduce((obj, key) => { - obj[key] = row[key]; - return obj; - }, {}), - ); - }, []); + return rawQuery( + `select + event_key as field, + count(*) as total + from event_data + where website_id = {websiteId:UUID} + and created_at >= ${getDateFormat(resetDate)} + and ${getBetweenDates('created_at', startDate, endDate)} + group by event_key + order by 2 desc, 1 asc + limit 1000 + `, + { websiteId }, + ); } diff --git a/yarn.lock b/yarn.lock index fc94a0f7..737c14e4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7344,10 +7344,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.89.0: - version "0.89.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.89.0.tgz#672a14448818fc7f20a3f7d73d0340d2165f94f2" - integrity sha512-nsYZCCfAjEy/fVt+5te3kQEyqA+4dEFutI9n7ol36eWmWbBJjZXCF1NgSHsosMYN2wlrpsrI7HoMTgL68FQnUg== +react-basics@^0.91.0: + version "0.91.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.91.0.tgz#2970529a22a455ec73a1be884eb93a109c9dafc0" + integrity sha512-vP8LYWiFwA+eguMEuHvHct4Jl5R/2GUjWc1tMujDG0CsAAUGhx68tAJr0K3gBrWjmpJrTPVfX8SdBNKSDAjQsw== dependencies: classnames "^2.3.1" date-fns "^2.29.3"