diff --git a/assets/expand.svg b/assets/expand.svg new file mode 100644 index 00000000..43b9036f --- /dev/null +++ b/assets/expand.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/lightbulb.svg b/assets/lightbulb.svg index c7895a7d..4ff96dcc 100644 --- a/assets/lightbulb.svg +++ b/assets/lightbulb.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/overview.svg b/assets/overview.svg new file mode 100644 index 00000000..ec44b4ea --- /dev/null +++ b/assets/overview.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/reports.svg b/assets/reports.svg new file mode 100644 index 00000000..66dfc325 --- /dev/null +++ b/assets/reports.svg @@ -0,0 +1 @@ + \ 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/icons.ts b/components/icons.ts index f3040bbd..efd6914b 100644 --- a/components/icons.ts +++ b/components/icons.ts @@ -10,7 +10,10 @@ import Globe from 'assets/globe.svg'; import Lock from 'assets/lock.svg'; import Logo from 'assets/logo.svg'; import Moon from 'assets/moon.svg'; +import Nodes from 'assets/nodes.svg'; +import Overview from 'assets/overview.svg'; import Profile from 'assets/profile.svg'; +import Reports from 'assets/reports.svg'; import Sun from 'assets/sun.svg'; import User from 'assets/user.svg'; import Users from 'assets/users.svg'; @@ -29,7 +32,10 @@ const icons = { Lock, Logo, Moon, + Nodes, + Overview, Profile, + Reports, Sun, User, Users, 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/Grid.module.css b/components/layout/Grid.module.css index 655e4ea2..dc2e8ff6 100644 --- a/components/layout/Grid.module.css +++ b/components/layout/Grid.module.css @@ -29,6 +29,7 @@ .row > .col { border-top: 1px solid var(--base300); + border-inline-start: 0; border-inline-end: 0; padding: 20px 0; } 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/messages.js b/components/messages.js index 7ddbdd2d..4fbad7a3 100644 --- a/components/messages.js +++ b/components/messages.js @@ -158,6 +158,8 @@ export const labels = defineMessages({ max: { id: 'labels.max', defaultMessage: 'Max' }, unique: { id: 'labels.unique', defaultMessage: 'Unique' }, value: { id: 'labels.value', defaultMessage: 'Value' }, + overview: { id: 'labels.overview', defaultMessage: 'Overview' }, + totalRecords: { id: 'labels.total-records', defaultMessage: 'Total records' }, }); export const messages = defineMessages({ diff --git a/components/metrics/BrowsersTable.js b/components/metrics/BrowsersTable.js index a8dd34ea..2920280f 100644 --- a/components/metrics/BrowsersTable.js +++ b/components/metrics/BrowsersTable.js @@ -2,12 +2,23 @@ import FilterLink from 'components/common/FilterLink'; import MetricsTable from 'components/metrics/MetricsTable'; import { BROWSERS } from 'lib/constants'; import useMessages from 'hooks/useMessages'; +import { useRouter } from 'next/router'; export function BrowsersTable({ websiteId, ...props }) { const { formatMessage, labels } = useMessages(); + const { basePath } = useRouter(); function renderLink({ x: browser }) { - return ; + return ( + + {browser} + + ); } return ( diff --git a/components/metrics/CountriesTable.js b/components/metrics/CountriesTable.js index 2d7b5ceb..c72d96ed 100644 --- a/components/metrics/CountriesTable.js +++ b/components/metrics/CountriesTable.js @@ -1,13 +1,15 @@ -import MetricsTable from './MetricsTable'; +import { useRouter } from 'next/router'; import FilterLink from 'components/common/FilterLink'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; import useMessages from 'hooks/useMessages'; +import MetricsTable from './MetricsTable'; export function CountriesTable({ websiteId, ...props }) { const { locale } = useLocale(); const countryNames = useCountryNames(locale); const { formatMessage, labels } = useMessages(); + const { basePath } = useRouter(); function renderLink({ x: code }) { return ( @@ -17,7 +19,7 @@ export function CountriesTable({ websiteId, ...props }) { value={countryNames[code] && code} label={countryNames[code]} > - {code} + {code} ); } diff --git a/components/metrics/DevicesTable.js b/components/metrics/DevicesTable.js index 1bc3ac04..30b2a742 100644 --- a/components/metrics/DevicesTable.js +++ b/components/metrics/DevicesTable.js @@ -1,9 +1,11 @@ import MetricsTable from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'hooks/useMessages'; +import { useRouter } from 'next/router'; export function DevicesTable({ websiteId, ...props }) { const { formatMessage, labels } = useMessages(); + const { basePath } = useRouter(); function renderLink({ x: device }) { return ( @@ -11,7 +13,14 @@ export function DevicesTable({ websiteId, ...props }) { id="device" value={labels[device] && device} label={formatMessage(labels[device] || labels.unknown)} - /> + > + {device} + ); } 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/OSTable.js b/components/metrics/OSTable.js index a638038b..02ead27d 100644 --- a/components/metrics/OSTable.js +++ b/components/metrics/OSTable.js @@ -1,12 +1,25 @@ import MetricsTable from './MetricsTable'; import FilterLink from 'components/common/FilterLink'; import useMessages from 'hooks/useMessages'; +import { useRouter } from 'next/router'; export function OSTable({ websiteId, ...props }) { const { formatMessage, labels } = useMessages(); + const { basePath } = useRouter(); function renderLink({ x: os }) { - return ; + return ( + + {os} + + ); } return ( diff --git a/components/metrics/RegionsTable.js b/components/metrics/RegionsTable.js index c8e0b768..d7b614f7 100644 --- a/components/metrics/RegionsTable.js +++ b/components/metrics/RegionsTable.js @@ -1,15 +1,17 @@ -import MetricsTable from './MetricsTable'; -import { emptyFilter } from 'lib/filters'; +import { useRouter } from 'next/router'; import FilterLink from 'components/common/FilterLink'; +import { emptyFilter } from 'lib/filters'; import useLocale from 'hooks/useLocale'; import useMessages from 'hooks/useMessages'; import useCountryNames from 'hooks/useCountryNames'; +import MetricsTable from './MetricsTable'; import regions from 'public/iso-3166-2.json'; export function RegionsTable({ websiteId, ...props }) { const { locale } = useLocale(); const { formatMessage, labels } = useMessages(); const countryNames = useCountryNames(locale); + const { basePath } = useRouter(); const renderLabel = x => { return regions[x] ? `${regions[x]}, ${countryNames[x.split('-')[0]]}` : x; @@ -18,7 +20,10 @@ export function RegionsTable({ websiteId, ...props }) { const renderLink = ({ x: code }) => { return ( - {code} + {code} ); }; 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 de21c0c7..00000000 --- a/components/metrics/WebsiteHeader.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Row, Column, Text } from 'react-basics'; -import Favicon from 'components/common/Favicon'; -import ActiveUsers from './ActiveUsers'; -import styles from './WebsiteHeader.module.css'; - -export function WebsiteHeader({ websiteId, name, domain, children }) { - return ( - - - - {name} - - - - {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..9bc20ead --- /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..43b14580 --- /dev/null +++ b/components/pages/event-data/EventDataMetricsBar.module.css @@ -0,0 +1,46 @@ +.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); + } +} + +.row { + border-bottom: 1px solid var(--border-color); +} diff --git a/components/pages/event-data/EventDataTable.js b/components/pages/event-data/EventDataTable.js new file mode 100644 index 00000000..e8cc5598 --- /dev/null +++ b/components/pages/event-data/EventDataTable.js @@ -0,0 +1,32 @@ +import Link from 'next/link'; +import { GridTable, GridColumn } from 'react-basics'; +import { useMessages, usePageQuery } from 'hooks'; +import Empty from 'components/common/Empty'; + +export function EventDataTable({ data = [], showValue }) { + const { formatMessage, labels } = useMessages(); + const { resolveUrl } = usePageQuery(); + + if (data.length === 0) { + return ; + } + + return ( + + + {row => { + return ( + + {row.field} + + ); + }} + + + {({ total }) => total.toLocaleString()} + + + ); +} + +export default EventDataTable; diff --git a/components/pages/event-data/EventDataValueTable.js b/components/pages/event-data/EventDataValueTable.js new file mode 100644 index 00000000..2a20c9b0 --- /dev/null +++ b/components/pages/event-data/EventDataValueTable.js @@ -0,0 +1,44 @@ +import { GridTable, GridColumn, Button, Icon, Text, Flexbox } from 'react-basics'; +import { useMessages, usePageQuery } from 'hooks'; +import Link from 'next/link'; +import Icons from 'components/icons'; +import PageHeader from 'components/layout/PageHeader'; +import Empty from 'components/common/Empty'; + +export function EventDataTable({ data = [], field }) { + const { formatMessage, labels } = useMessages(); + const { resolveUrl } = usePageQuery(); + + const Title = () => { + return ( + <> + + + + {field} + + ); + }; + + return ( + <> + } /> + {data.length <= 0 && } + {data.length > 0 && ( + + + + {({ total }) => total.toLocaleString()} + + + )} + + ); +} + +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/ReportTemplates.js b/components/pages/reports/ReportTemplates.js index 7ed40af7..35ba6f01 100644 --- a/components/pages/reports/ReportTemplates.js +++ b/components/pages/reports/ReportTemplates.js @@ -9,24 +9,12 @@ import styles from './ReportTemplates.module.css'; import { useMessages } from 'hooks'; const reports = [ - { - title: 'Event data', - description: 'Query your custom event data.', - url: '/reports/event-data', - icon: , - }, { title: 'Funnel', description: 'Understand the conversion and drop-off rate of users.', url: '/reports/funnel', icon: , }, - { - title: 'Insights', - description: 'Explore your data by applying segments and filters.', - url: '/reports/insights', - icon: , - }, ]; function ReportItem({ title, description, url, icon }) { diff --git a/components/pages/reports/ReportTemplates.module.css b/components/pages/reports/ReportTemplates.module.css index 0cdcb835..33183505 100644 --- a/components/pages/reports/ReportTemplates.module.css +++ b/components/pages/reports/ReportTemplates.module.css @@ -2,6 +2,7 @@ display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 20px; + width: 360px; } .report { 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 ( - +
); }} diff --git a/components/pages/reports/event-data/EventDataReport.js b/components/pages/reports/event-data/EventDataReport.js index b3358ecf..eb49a29d 100644 --- a/components/pages/reports/event-data/EventDataReport.js +++ b/components/pages/reports/event-data/EventDataReport.js @@ -3,12 +3,12 @@ import ReportHeader from '../ReportHeader'; import ReportMenu from '../ReportMenu'; import ReportBody from '../ReportBody'; import EventDataParameters from './EventDataParameters'; -import Nodes from 'assets/nodes.svg'; import EventDataTable from './EventDataTable'; +import Nodes from 'assets/nodes.svg'; const defaultParameters = { type: 'event-data', - parameters: { fields: [], filters: [], groups: [] }, + parameters: { fields: [], filters: [] }, }; export default function EventDataReport({ reportId }) { diff --git a/components/pages/reports/insights/FieldAddForm.js b/components/pages/reports/insights/FieldAddForm.js new file mode 100644 index 00000000..c95fcac3 --- /dev/null +++ b/components/pages/reports/insights/FieldAddForm.js @@ -0,0 +1,44 @@ +import { useState } from 'react'; +import { createPortal } from 'react-dom'; +import { REPORT_PARAMETERS } from 'lib/constants'; +import PopupForm from '../PopupForm'; +import FieldSelectForm from '../FieldSelectForm'; +import FieldAggregateForm from '../FieldAggregateForm'; +import FieldFilterForm from '../FieldFilterForm'; +import styles from './FieldAddForm.module.css'; + +export function FieldAddForm({ fields = [], group, element, onAdd, onClose }) { + const [selected, setSelected] = useState(); + + const handleSelect = value => { + const { type } = value; + + if (group === REPORT_PARAMETERS.groups || type === 'array' || type === 'boolean') { + value.value = group === REPORT_PARAMETERS.groups ? '' : 'total'; + handleSave(value); + return; + } + + setSelected(value); + }; + + const handleSave = value => { + onAdd(group, value); + onClose(); + }; + + return createPortal( + + {!selected && } + {selected && group === REPORT_PARAMETERS.fields && ( + + )} + {selected && group === REPORT_PARAMETERS.filters && ( + + )} + , + document.body, + ); +} + +export default FieldAddForm; diff --git a/components/pages/reports/insights/FieldAddForm.module.css b/components/pages/reports/insights/FieldAddForm.module.css new file mode 100644 index 00000000..5c5aaa4f --- /dev/null +++ b/components/pages/reports/insights/FieldAddForm.module.css @@ -0,0 +1,38 @@ +.menu { + width: 360px; + max-height: 300px; + overflow: auto; +} + +.item { + display: flex; + flex-direction: row; + justify-content: space-between; + border-radius: var(--border-radius); +} + +.item:hover { + background: var(--base75); +} + +.type { + color: var(--font-color300); +} + +.selected { + font-weight: bold; +} + +.popup { + display: flex; +} + +.filter { + display: flex; + flex-direction: column; + gap: 20px; +} + +.dropdown { + min-width: 60px; +} diff --git a/components/pages/reports/insights/InsightsParameters.js b/components/pages/reports/insights/InsightsParameters.js new file mode 100644 index 00000000..39bfc2e8 --- /dev/null +++ b/components/pages/reports/insights/InsightsParameters.js @@ -0,0 +1,151 @@ +import { useContext, useRef } from 'react'; +import { useApi, useMessages } from 'hooks'; +import { Form, FormRow, FormButtons, SubmitButton, PopupTrigger, Icon, Popup } from 'react-basics'; +import { ReportContext } from 'components/pages/reports/Report'; +import Empty from 'components/common/Empty'; +import { DATA_TYPES, REPORT_PARAMETERS } from 'lib/constants'; +import Icons from 'components/icons'; +import FieldAddForm from './FieldAddForm'; +import BaseParameters from '../BaseParameters'; +import ParameterList from '../ParameterList'; +import styles from './InsightsParameters.module.css'; + +function useFields(websiteId, startDate, endDate) { + const { get, useQuery } = useApi(); + const { data, error, isLoading } = useQuery( + ['fields', websiteId, startDate, endDate], + () => + get('/reports/event-data', { + websiteId, + startAt: +startDate, + endAt: +endDate, + }), + { enabled: !!(websiteId && startDate && endDate) }, + ); + + return { data, error, isLoading }; +} + +export function InsightsParameters() { + const { report, runReport, updateReport, isRunning } = useContext(ReportContext); + const { formatMessage, labels, messages } = useMessages(); + const ref = useRef(null); + const { parameters } = report || {}; + const { websiteId, dateRange, fields, filters, groups } = parameters || {}; + const { startDate, endDate } = dateRange || {}; + const queryEnabled = websiteId && dateRange && fields?.length; + const { data, error } = useFields(websiteId, startDate, endDate); + const parametersSelected = websiteId && startDate && endDate; + const hasData = data?.length !== 0; + + const parameterGroups = [ + { label: formatMessage(labels.fields), group: REPORT_PARAMETERS.fields }, + { label: formatMessage(labels.filters), group: REPORT_PARAMETERS.filters }, + { label: formatMessage(labels.breakdown), group: REPORT_PARAMETERS.groups }, + ]; + + const parameterData = { + fields, + filters, + groups, + }; + + const handleSubmit = values => { + runReport(values); + }; + + const handleAdd = (group, value) => { + const data = parameterData[group]; + + if (!data.find(({ name }) => name === value.name)) { + updateReport({ parameters: { [group]: data.concat(value) } }); + } + }; + + const handleRemove = (group, index) => { + const data = [...parameterData[group]]; + data.splice(index, 1); + updateReport({ parameters: { [group]: data } }); + }; + + const AddButton = ({ group }) => { + return ( + + + + + + {(close, element) => { + return ( + ({ + name: eventKey, + type: DATA_TYPES[InsightsType], + }))} + group={group} + element={element} + onAdd={handleAdd} + onClose={close} + /> + ); + }} + + + ); + }; + + return ( +
+ + {!hasData && } + {parametersSelected && + hasData && + parameterGroups.map(({ label, group }) => { + return ( + } + > + handleRemove(group, index)} + > + {({ name, value }) => { + return ( +
+ {group === REPORT_PARAMETERS.fields && ( + <> +
{name}
+
{value}
+ + )} + {group === REPORT_PARAMETERS.filters && ( + <> +
{name}
+
{value[0]}
+
{value[1]}
+ + )} + {group === REPORT_PARAMETERS.groups && ( + <> +
{name}
+ + )} +
+ ); + }} +
+
+ ); + })} + + + {formatMessage(labels.runQuery)} + + + + ); +} + +export default InsightsParameters; diff --git a/components/pages/reports/insights/InsightsParameters.module.css b/components/pages/reports/insights/InsightsParameters.module.css new file mode 100644 index 00000000..06b62414 --- /dev/null +++ b/components/pages/reports/insights/InsightsParameters.module.css @@ -0,0 +1,12 @@ +.parameter { + display: flex; + gap: 10px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + min-width: 0; +} + +.op { + font-weight: bold; +} diff --git a/components/pages/reports/insights/InsightsReport.js b/components/pages/reports/insights/InsightsReport.js new file mode 100644 index 00000000..88f12304 --- /dev/null +++ b/components/pages/reports/insights/InsightsReport.js @@ -0,0 +1,26 @@ +import Report from '../Report'; +import ReportHeader from '../ReportHeader'; +import ReportMenu from '../ReportMenu'; +import ReportBody from '../ReportBody'; +import InsightsParameters from './InsightsParameters'; +import InsightsTable from './InsightsTable'; +import Lightbulb from 'assets/lightbulb.svg'; + +const defaultParameters = { + type: 'insights', + parameters: { fields: [], filters: [], groups: [] }, +}; + +export default function InsightsReport({ reportId }) { + return ( + + } /> + + + + + + + + ); +} diff --git a/components/pages/reports/insights/InsightsTable.js b/components/pages/reports/insights/InsightsTable.js new file mode 100644 index 00000000..a767468e --- /dev/null +++ b/components/pages/reports/insights/InsightsTable.js @@ -0,0 +1,19 @@ +import { useContext } from 'react'; +import { GridTable, GridColumn } from 'react-basics'; +import { useMessages } from 'hooks'; +import { ReportContext } from '../Report'; + +export function InsightsTable() { + const { report } = useContext(ReportContext); + const { formatMessage, labels } = useMessages(); + + return ( + + + + + + ); +} + +export default InsightsTable; diff --git a/components/pages/websites/WebsiteChart.js b/components/pages/websites/WebsiteChart.js new file mode 100644 index 00000000..12b8ab4b --- /dev/null +++ b/components/pages/websites/WebsiteChart.js @@ -0,0 +1,59 @@ +import { useMemo } from 'react'; +import PageviewsChart from 'components/metrics/PageviewsChart'; +import { useApi, useDateRange, useTimezone, usePageQuery } from 'hooks'; +import { getDateArray, getDateLength } from 'lib/date'; + +export function WebsiteChart({ websiteId }) { + const [dateRange] = useDateRange(websiteId); + const { startDate, endDate, unit, modified } = dateRange; + const [timezone] = useTimezone(); + const { + query: { url, referrer, os, browser, device, country, region, city, title }, + } = usePageQuery(); + const { get, useQuery } = useApi(); + + const { data, isLoading } = 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, + }), + ); + + const chartData = useMemo(() => { + if (data) { + return { + pageviews: getDateArray(data.pageviews, startDate, endDate, unit), + sessions: getDateArray(data.sessions, startDate, endDate, unit), + }; + } + return { pageviews: [], sessions: [] }; + }, [data, startDate, endDate, unit, modified]); + + return ( + + ); +} + +export default WebsiteChart; diff --git a/components/pages/websites/WebsiteChart.module.css b/components/pages/websites/WebsiteChart.module.css new file mode 100644 index 00000000..b795047a --- /dev/null +++ b/components/pages/websites/WebsiteChart.module.css @@ -0,0 +1,17 @@ +.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; +} diff --git a/components/pages/websites/WebsiteChartList.js b/components/pages/websites/WebsiteChartList.js index 0c552704..42079a53 100644 --- a/components/pages/websites/WebsiteChartList.js +++ b/components/pages/websites/WebsiteChartList.js @@ -1,11 +1,19 @@ +import { Button, Text, Icon } from 'react-basics'; import { useMemo } from 'react'; import { firstBy } from 'thenby'; -import WebsiteChart from 'components/metrics/WebsiteChart'; +import Link from 'next/link'; +import WebsiteChart from 'components/pages/websites/WebsiteChart'; import useDashboard from 'store/dashboard'; import styles from './WebsiteList.module.css'; +import WebsiteHeader from './WebsiteHeader'; +import { WebsiteMetricsBar } from './WebsiteMetricsBar'; +import { useMessages, useLocale } from 'hooks'; +import Icons from 'components/icons'; export default function WebsiteChartList({ websites, showCharts, limit }) { + const { formatMessage, labels } = useMessages(); const { websiteOrder } = useDashboard(); + const { dir } = useLocale(); const ordered = useMemo( () => @@ -17,16 +25,23 @@ export default function WebsiteChartList({ websites, showCharts, limit }) { return (
- {ordered.map(({ id, name, domain }, index) => { + {ordered.map(({ id }, index) => { return index < limit ? (
- + + + + + + +
) : 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..4ec18cb1 --- /dev/null +++ b/components/pages/websites/WebsiteEventData.js @@ -0,0 +1,39 @@ +import { Flexbox } from 'react-basics'; +import EventDataTable from 'components/pages/event-data/EventDataTable'; +import EventDataValueTable from 'components/pages/event-data/EventDataValueTable'; +import { EventDataMetricsBar } from 'components/pages/event-data/EventDataMetricsBar'; +import { useDateRange, useApi, usePageQuery } from 'hooks'; + +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, field }], + () => + get('/event-data', { + websiteId, + startAt: +startDate, + endAt: +endDate, + field, + }), + { enabled: !!(websiteId && startDate && endDate) }, + ); + + return { data, error, isLoading }; +} + +export default function WebsiteEventData({ websiteId }) { + const { + query: { view }, + } = usePageQuery(); + const { data } = useFields(websiteId, view); + + return ( + + + {!view && } + {view && } + + ); +} diff --git a/components/pages/websites/WebsiteEventData.module.css b/components/pages/websites/WebsiteEventData.module.css new file mode 100644 index 00000000..a386e82a --- /dev/null +++ b/components/pages/websites/WebsiteEventData.module.css @@ -0,0 +1,4 @@ +.container { + display: flex; + flex-direction: column; +} 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 88% rename from components/metrics/WebsiteHeader.module.css rename to components/pages/websites/WebsiteHeader.module.css index 68fd22f8..89f78e52 100644 --- a/components/metrics/WebsiteHeader.module.css +++ b/components/pages/websites/WebsiteHeader.module.css @@ -15,7 +15,7 @@ height: 100px; } -.info { +.actions { display: flex; flex-direction: row; align-items: center; @@ -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/lib/constants.ts b/lib/constants.ts index 5772e147..209a97b6 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -166,8 +166,9 @@ export const EVENT_COLORS = [ '#ffec16', ]; -export const DOMAIN_REGEX = - /^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,63})$/; +export const DOMAIN_REGEX = + /^(localhost(:[1-9]\d{0,4})?|((?=[a-z0-9-]{1,63}\.)(xn--)?[a-z0-9-]+(-[a-z0-9-]+)*\.)+(xn--)?[a-z0-9-]{2,63})$/; + export const SHARE_ID_REGEX = /^[a-zA-Z0-9]{16}$/; @@ -182,7 +183,7 @@ export const DESKTOP_OS = [ 'BeOS', 'Chrome OS', 'Linux', - 'macOS', + 'Mac OS', 'Open BSD', 'OS/2', 'QNX', @@ -204,33 +205,34 @@ export const DESKTOP_OS = [ export const MOBILE_OS = ['Amazon OS', 'Android OS', 'BlackBerry OS', 'iOS', 'Windows Mobile']; export const BROWSERS = { + android: 'Android', aol: 'AOL', - edge: 'Edge', - 'edge-ios': 'Edge (iOS)', - yandexbrowser: 'Yandex', - kakaotalk: 'KaKaoTalk', - samsung: 'Samsung', - silk: 'Silk', - miui: 'MIUI', beaker: 'Beaker', - 'edge-chromium': 'Edge (Chromium)', + bb10: 'BlackBerry 10', chrome: 'Chrome', 'chromium-webview': 'Chrome (webview)', - phantomjs: 'PhantomJS', crios: 'Chrome (iOS)', + curl: 'Curl', + edge: 'Edge', + 'edge-chromium': 'Edge (Chromium)', + 'edge-ios': 'Edge (iOS)', + facebook: 'Facebook', firefox: 'Firefox', fxios: 'Firefox (iOS)', - 'opera-mini': 'Opera Mini', - opera: 'Opera', ie: 'IE', - bb10: 'BlackBerry 10', - android: 'Android', - ios: 'iOS', - safari: 'Safari', - facebook: 'Facebook', instagram: 'Instagram', + ios: 'iOS', 'ios-webview': 'iOS (webview)', + kakaotalk: 'KaKaoTalk', + miui: 'MIUI', + opera: 'Opera', + 'opera-mini': 'Opera Mini', + phantomjs: 'PhantomJS', + safari: 'Safari', + samsung: 'Samsung', + silk: 'Silk', searchbot: 'Searchbot', + yandexbrowser: 'Yandex', }; export const MAP_FILE = '/datamaps.world.json'; diff --git a/package.json b/package.json index 6d5ed0fe..e69052d7 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", @@ -105,7 +105,7 @@ "react-use-measure": "^2.0.4", "react-window": "^1.8.6", "request-ip": "^3.3.0", - "semver": "^7.3.6", + "semver": "^7.5.2", "thenby": "^1.3.4", "timezone-support": "^2.0.2", "uuid": "^8.3.2", @@ -151,7 +151,7 @@ "rollup-plugin-node-externals": "^5.1.2", "rollup-plugin-postcss": "^4.0.2", "rollup-plugin-terser": "^7.0.2", - "stylelint": "^14.16.1", + "stylelint": "^15.10.1", "stylelint-config-css-modules": "^4.1.0", "stylelint-config-prettier": "^9.0.3", "stylelint-config-recommended": "^9.0.0", 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/console/[[...id]].js b/pages/console/[[...id]].js index b4bcf254..d13d6f68 100644 --- a/pages/console/[[...id]].js +++ b/pages/console/[[...id]].js @@ -1,7 +1,7 @@ import AppLayout from 'components/layout/AppLayout'; import TestConsole from 'components/pages/console/TestConsole'; -export default function ConsolePage({ disabled }) { +export default function ({ disabled }) { if (disabled) { return null; } diff --git a/pages/index.js b/pages/index.js index 7d93cef1..bd4c74be 100644 --- a/pages/index.js +++ b/pages/index.js @@ -1,7 +1,7 @@ import { useEffect } from 'react'; import { useRouter } from 'next/router'; -export default function DefaultPage() { +export default function () { const router = useRouter(); useEffect(() => { diff --git a/pages/login.js b/pages/login.js index 9a1f3c45..a43f8c1f 100644 --- a/pages/login.js +++ b/pages/login.js @@ -1,7 +1,7 @@ import LoginLayout from 'components/pages/login/LoginLayout'; import LoginForm from 'components/pages/login/LoginForm'; -export default function LoginPage({ disabled }) { +export default function ({ disabled }) { if (disabled) { return null; } diff --git a/pages/logout.js b/pages/logout.js index 6ffe23e1..675f1932 100644 --- a/pages/logout.js +++ b/pages/logout.js @@ -4,7 +4,7 @@ import useApi from 'hooks/useApi'; import { setUser } from 'store/app'; import { removeClientAuthToken } from 'lib/client'; -export default function LogoutPage({ disabled }) { +export default function ({ disabled }) { const router = useRouter(); const { post } = useApi(); diff --git a/pages/realtime/[id].js b/pages/realtime/[id].js deleted file mode 100644 index c9e82622..00000000 --- a/pages/realtime/[id].js +++ /dev/null @@ -1,26 +0,0 @@ -import { useRouter } from 'next/router'; -import AppLayout from 'components/layout/AppLayout'; -import RealtimeDashboard from 'components/pages/realtime/RealtimeDashboard'; -import useMessages from 'hooks/useMessages'; -import useApi from 'hooks/useApi'; - -export default function RealtimeDetailsPage() { - const router = useRouter(); - const { id: websiteId } = router.query; - const { formatMessage, labels } = useMessages(); - const { get, useQuery } = useApi(); - const { data: website } = useQuery(['websites', websiteId], () => get(`/websites/${websiteId}`), { - enabled: !!websiteId, - }); - const title = `${formatMessage(labels.realtime)}${website?.name ? ` - ${website.name}` : ''}`; - - if (!websiteId) { - return null; - } - - return ( - - - - ); -} diff --git a/pages/realtime/index.js b/pages/realtime/index.js deleted file mode 100644 index bb016173..00000000 --- a/pages/realtime/index.js +++ /dev/null @@ -1,12 +0,0 @@ -import AppLayout from 'components/layout/AppLayout'; -import RealtimeHome from 'components/pages/realtime/RealtimeHome'; -import useMessages from 'hooks/useMessages'; - -export default function RealtimePage() { - const { formatMessage, labels } = useMessages(); - return ( - - - - ); -} diff --git a/pages/reports/[id].js b/pages/reports/[id].js index 36a84a2e..2520e87d 100644 --- a/pages/reports/[id].js +++ b/pages/reports/[id].js @@ -3,7 +3,7 @@ import AppLayout from 'components/layout/AppLayout'; import ReportDetails from 'components/pages/reports/ReportDetails'; import { useApi, useMessages } from 'hooks'; -export default function ReportsPage() { +export default function () { const { formatMessage, labels } = useMessages(); const router = useRouter(); const { id } = router.query; diff --git a/pages/reports/create.js b/pages/reports/create.js index 421b9c0a..763e2c63 100644 --- a/pages/reports/create.js +++ b/pages/reports/create.js @@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout'; import ReportTemplates from 'components/pages/reports/ReportTemplates'; import { useMessages } from 'hooks'; -export default function ReportsPage() { +export default function () { const { formatMessage, labels } = useMessages(); return ( diff --git a/pages/reports/event-data.js b/pages/reports/event-data.js index b5749c96..4566b320 100644 --- a/pages/reports/event-data.js +++ b/pages/reports/event-data.js @@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout'; import EventDataReport from 'components/pages/reports/event-data/EventDataReport'; import { useMessages } from 'hooks'; -export default function Report() { +export default function () { const { formatMessage, labels } = useMessages(); return ( diff --git a/pages/reports/funnel.js b/pages/reports/funnel.js index 731ffa47..4acdef37 100644 --- a/pages/reports/funnel.js +++ b/pages/reports/funnel.js @@ -2,7 +2,7 @@ import AppLayout from 'components/layout/AppLayout'; import FunnelReport from 'components/pages/reports/funnel/FunnelReport'; import useMessages from 'hooks/useMessages'; -export default function Funnel() { +export default function () { const { formatMessage, labels } = useMessages(); return ( diff --git a/pages/reports/index.js b/pages/reports/index.js deleted file mode 100644 index 237f31ee..00000000 --- a/pages/reports/index.js +++ /dev/null @@ -1,13 +0,0 @@ -import AppLayout from 'components/layout/AppLayout'; -import useMessages from 'hooks/useMessages'; -import ReportsList from 'components/pages/reports/ReportsList'; - -export default function ReportsPage() { - const { formatMessage, labels } = useMessages(); - - return ( - - - - ); -} diff --git a/pages/settings/profile/index.js b/pages/settings/profile/index.js index aef21bb6..8827f1da 100644 --- a/pages/settings/profile/index.js +++ b/pages/settings/profile/index.js @@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout'; import ProfileSettings from 'components/pages/settings/profile/ProfileSettings'; import useMessages from 'hooks/useMessages'; -export default function ProfilePage() { +export default function () { const { formatMessage, labels } = useMessages(); return ( diff --git a/pages/settings/teams/[id].js b/pages/settings/teams/[id].js index 6eb631b1..a68ef80c 100644 --- a/pages/settings/teams/[id].js +++ b/pages/settings/teams/[id].js @@ -4,7 +4,7 @@ import TeamSettings from 'components/pages/settings/teams/TeamSettings'; import { useRouter } from 'next/router'; import useMessages from 'hooks/useMessages'; -export default function TeamDetailPage({ disabled }) { +export default function ({ disabled }) { const router = useRouter(); const { id } = router.query; const { formatMessage, labels } = useMessages(); diff --git a/pages/settings/teams/index.js b/pages/settings/teams/index.js index 471ddf0f..51739c31 100644 --- a/pages/settings/teams/index.js +++ b/pages/settings/teams/index.js @@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout'; import TeamsList from 'components/pages/settings/teams/TeamsList'; import useMessages from 'hooks/useMessages'; -export default function TeamsPage({ disabled }) { +export default function ({ disabled }) { const { formatMessage, labels } = useMessages(); if (disabled) { return null; diff --git a/pages/settings/users/[id].js b/pages/settings/users/[id].js index 94df6951..d1e53419 100644 --- a/pages/settings/users/[id].js +++ b/pages/settings/users/[id].js @@ -4,7 +4,7 @@ import UserSettings from 'components/pages/settings/users/UserSettings'; import { useRouter } from 'next/router'; import useMessages from 'hooks/useMessages'; -export default function TeamDetailPage({ disabled }) { +export default function ({ disabled }) { const router = useRouter(); const { id } = router.query; const { formatMessage, labels } = useMessages(); diff --git a/pages/settings/users/index.js b/pages/settings/users/index.js index 6e021a74..ee325adc 100644 --- a/pages/settings/users/index.js +++ b/pages/settings/users/index.js @@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout'; import UsersList from 'components/pages/settings/users/UsersList'; import useMessages from 'hooks/useMessages'; -export default function UsersPage({ disabled }) { +export default function ({ disabled }) { const { formatMessage, labels } = useMessages(); if (disabled) { return null; diff --git a/pages/settings/websites/[id].js b/pages/settings/websites/[id].js index 7b97b8f5..f828369e 100644 --- a/pages/settings/websites/[id].js +++ b/pages/settings/websites/[id].js @@ -4,7 +4,7 @@ import WebsiteSettings from 'components/pages/settings/websites/WebsiteSettings' import SettingsLayout from 'components/layout/SettingsLayout'; import useMessages from 'hooks/useMessages'; -export default function WebsiteSettingsPage({ disabled }) { +export default function ({ disabled }) { const router = useRouter(); const { id } = router.query; const { formatMessage, labels } = useMessages(); diff --git a/pages/settings/websites/index.js b/pages/settings/websites/index.js index c115b081..899ad7c7 100644 --- a/pages/settings/websites/index.js +++ b/pages/settings/websites/index.js @@ -3,7 +3,7 @@ import SettingsLayout from 'components/layout/SettingsLayout'; import WebsitesList from 'components/pages/settings/websites/WebsitesList'; import useMessages from 'hooks/useMessages'; -export default function WebsitesPage({ disabled }) { +export default function ({ disabled }) { const { formatMessage, labels } = useMessages(); if (disabled) { return null; diff --git a/pages/share/[...id].js b/pages/share/[...id].js index aa6caab3..1e424382 100644 --- a/pages/share/[...id].js +++ b/pages/share/[...id].js @@ -1,9 +1,9 @@ import { useRouter } from 'next/router'; import ShareLayout from 'components/layout/ShareLayout'; -import WebsiteDetails from 'components/pages/websites/WebsiteDetails'; +import WebsiteDetailsPage from 'components/pages/websites/WebsiteDetailsPage'; import useShareToken from 'hooks/useShareToken'; -export default function SharePage() { +export default function () { const router = useRouter(); const { id } = router.query; const shareId = id?.[0]; @@ -15,7 +15,7 @@ export default function SharePage() { return ( - + ); } diff --git a/pages/sso.js b/pages/sso.js index c3d499c3..6e635206 100644 --- a/pages/sso.js +++ b/pages/sso.js @@ -3,7 +3,7 @@ import { Loading } from 'react-basics'; import { useRouter } from 'next/router'; import { setClientAuthToken } from 'lib/client'; -export default function SingleSignOnPage() { +export default function () { const router = useRouter(); const { token, url } = router.query; diff --git a/pages/websites/[id]/event-data.js b/pages/websites/[id]/event-data.js new file mode 100644 index 00000000..8b44616d --- /dev/null +++ b/pages/websites/[id]/event-data.js @@ -0,0 +1,20 @@ +import { useRouter } from 'next/router'; +import AppLayout from 'components/layout/AppLayout'; +import WebsiteEventDataPage from 'components/pages/websites/WebsiteEventDataPage'; +import useMessages from 'hooks/useMessages'; + +export default function () { + const { formatMessage, labels } = useMessages(); + const router = useRouter(); + const { id } = router.query; + + if (!id) { + return null; + } + + return ( + + + + ); +} diff --git a/pages/websites/[id].js b/pages/websites/[id]/index.js similarity index 71% rename from pages/websites/[id].js rename to pages/websites/[id]/index.js index d2a258e1..bec7a45f 100644 --- a/pages/websites/[id].js +++ b/pages/websites/[id]/index.js @@ -1,9 +1,9 @@ import { useRouter } from 'next/router'; import AppLayout from 'components/layout/AppLayout'; -import WebsiteDetails from 'components/pages/websites/WebsiteDetails'; +import WebsiteDetailsPage from 'components/pages/websites/WebsiteDetailsPage'; import useMessages from 'hooks/useMessages'; -export default function DetailsPage() { +export default function () { const { formatMessage, labels } = useMessages(); const router = useRouter(); const { id } = router.query; @@ -14,7 +14,7 @@ export default function DetailsPage() { return ( - + ); } diff --git a/pages/websites/[id]/realtime.js b/pages/websites/[id]/realtime.js new file mode 100644 index 00000000..efe486a5 --- /dev/null +++ b/pages/websites/[id]/realtime.js @@ -0,0 +1,18 @@ +import { useRouter } from 'next/router'; +import AppLayout from 'components/layout/AppLayout'; +import RealtimePage from 'components/pages/realtime/RealtimePage'; + +export default function () { + const router = useRouter(); + const { id: websiteId } = router.query; + + if (!websiteId) { + return null; + } + + return ( + + + + ); +} diff --git a/pages/websites/[id]/reports.js b/pages/websites/[id]/reports.js new file mode 100644 index 00000000..ccd88081 --- /dev/null +++ b/pages/websites/[id]/reports.js @@ -0,0 +1,18 @@ +import { useRouter } from 'next/router'; +import AppLayout from 'components/layout/AppLayout'; +import WebsiteReportsPage from 'components/pages/websites/WebsiteReportsPage'; + +export default function () { + const router = useRouter(); + const { id: websiteId } = router.query; + + if (!websiteId) { + return null; + } + + return ( + + + + ); +} diff --git a/public/images/browsers/android-webview.png b/public/images/browsers/android-webview.png new file mode 100644 index 00000000..99242297 Binary files /dev/null and b/public/images/browsers/android-webview.png differ diff --git a/public/images/browsers/android.png b/public/images/browsers/android.png new file mode 100644 index 00000000..6e28498d Binary files /dev/null and b/public/images/browsers/android.png differ diff --git a/public/images/browsers/aol.png b/public/images/browsers/aol.png new file mode 100644 index 00000000..66dc4288 Binary files /dev/null and b/public/images/browsers/aol.png differ diff --git a/public/images/browsers/beaker.png b/public/images/browsers/beaker.png new file mode 100644 index 00000000..fbc997cd Binary files /dev/null and b/public/images/browsers/beaker.png differ diff --git a/public/images/browsers/blackberry.png b/public/images/browsers/blackberry.png new file mode 100644 index 00000000..74f255cb Binary files /dev/null and b/public/images/browsers/blackberry.png differ diff --git a/public/images/browsers/brave.png b/public/images/browsers/brave.png new file mode 100644 index 00000000..0556c120 Binary files /dev/null and b/public/images/browsers/brave.png differ diff --git a/public/images/browsers/chrome.png b/public/images/browsers/chrome.png new file mode 100644 index 00000000..e4e2773f Binary files /dev/null and b/public/images/browsers/chrome.png differ diff --git a/public/images/browsers/chromium-webview.png b/public/images/browsers/chromium-webview.png new file mode 100644 index 00000000..a3fd998d Binary files /dev/null and b/public/images/browsers/chromium-webview.png differ diff --git a/public/images/browsers/crios.png b/public/images/browsers/crios.png new file mode 100644 index 00000000..e4e2773f Binary files /dev/null and b/public/images/browsers/crios.png differ diff --git a/public/images/browsers/curl.png b/public/images/browsers/curl.png new file mode 100644 index 00000000..dd221927 Binary files /dev/null and b/public/images/browsers/curl.png differ diff --git a/public/images/browsers/edge-chromium.png b/public/images/browsers/edge-chromium.png new file mode 100644 index 00000000..1f2b230f Binary files /dev/null and b/public/images/browsers/edge-chromium.png differ diff --git a/public/images/browsers/edge-ios.png b/public/images/browsers/edge-ios.png new file mode 100644 index 00000000..1f2b230f Binary files /dev/null and b/public/images/browsers/edge-ios.png differ diff --git a/public/images/browsers/edge.png b/public/images/browsers/edge.png new file mode 100644 index 00000000..3881a7e0 Binary files /dev/null and b/public/images/browsers/edge.png differ diff --git a/public/images/browsers/facebook.png b/public/images/browsers/facebook.png new file mode 100644 index 00000000..4dc9b267 Binary files /dev/null and b/public/images/browsers/facebook.png differ diff --git a/public/images/browsers/firefox.png b/public/images/browsers/firefox.png new file mode 100644 index 00000000..c118f9c1 Binary files /dev/null and b/public/images/browsers/firefox.png differ diff --git a/public/images/browsers/fxios.png b/public/images/browsers/fxios.png new file mode 100644 index 00000000..c118f9c1 Binary files /dev/null and b/public/images/browsers/fxios.png differ diff --git a/public/images/browsers/ie.png b/public/images/browsers/ie.png new file mode 100644 index 00000000..1d3bbe8f Binary files /dev/null and b/public/images/browsers/ie.png differ diff --git a/public/images/browsers/instagram.png b/public/images/browsers/instagram.png new file mode 100644 index 00000000..5961a6b3 Binary files /dev/null and b/public/images/browsers/instagram.png differ diff --git a/public/images/browsers/ios-webview.png b/public/images/browsers/ios-webview.png new file mode 100644 index 00000000..5f2dd401 Binary files /dev/null and b/public/images/browsers/ios-webview.png differ diff --git a/public/images/browsers/ios.png b/public/images/browsers/ios.png new file mode 100644 index 00000000..5f2dd401 Binary files /dev/null and b/public/images/browsers/ios.png differ diff --git a/public/images/browsers/kakaotalk.png b/public/images/browsers/kakaotalk.png new file mode 100644 index 00000000..e932a67b Binary files /dev/null and b/public/images/browsers/kakaotalk.png differ diff --git a/public/images/browsers/miui.png b/public/images/browsers/miui.png new file mode 100644 index 00000000..5f929510 Binary files /dev/null and b/public/images/browsers/miui.png differ diff --git a/public/images/browsers/opera-mini.png b/public/images/browsers/opera-mini.png new file mode 100644 index 00000000..d4e26712 Binary files /dev/null and b/public/images/browsers/opera-mini.png differ diff --git a/public/images/browsers/opera.png b/public/images/browsers/opera.png new file mode 100644 index 00000000..84e6d0fc Binary files /dev/null and b/public/images/browsers/opera.png differ diff --git a/public/images/browsers/safari.png b/public/images/browsers/safari.png new file mode 100644 index 00000000..b06369aa Binary files /dev/null and b/public/images/browsers/safari.png differ diff --git a/public/images/browsers/samsung.png b/public/images/browsers/samsung.png new file mode 100644 index 00000000..544e390e Binary files /dev/null and b/public/images/browsers/samsung.png differ diff --git a/public/images/browsers/searchbot.png b/public/images/browsers/searchbot.png new file mode 100644 index 00000000..46a33055 Binary files /dev/null and b/public/images/browsers/searchbot.png differ diff --git a/public/images/browsers/silk.png b/public/images/browsers/silk.png new file mode 100644 index 00000000..6af1d726 Binary files /dev/null and b/public/images/browsers/silk.png differ diff --git a/public/images/browsers/unknown.png b/public/images/browsers/unknown.png new file mode 100644 index 00000000..52058026 Binary files /dev/null and b/public/images/browsers/unknown.png differ diff --git a/public/images/browsers/yandexbrowser.png b/public/images/browsers/yandexbrowser.png new file mode 100644 index 00000000..f703db23 Binary files /dev/null and b/public/images/browsers/yandexbrowser.png differ diff --git a/public/images/device/desktop.png b/public/images/device/desktop.png new file mode 100644 index 00000000..d5ede419 Binary files /dev/null and b/public/images/device/desktop.png differ diff --git a/public/images/device/laptop.png b/public/images/device/laptop.png new file mode 100644 index 00000000..19f66967 Binary files /dev/null and b/public/images/device/laptop.png differ diff --git a/public/images/device/mobile.png b/public/images/device/mobile.png new file mode 100644 index 00000000..d2190f4a Binary files /dev/null and b/public/images/device/mobile.png differ diff --git a/public/images/device/tablet.png b/public/images/device/tablet.png new file mode 100644 index 00000000..5e06bcff Binary files /dev/null and b/public/images/device/tablet.png differ diff --git a/public/images/device/unknown.png b/public/images/device/unknown.png new file mode 100644 index 00000000..52058026 Binary files /dev/null and b/public/images/device/unknown.png differ diff --git a/public/images/os/amazon-os.png b/public/images/os/amazon-os.png new file mode 100644 index 00000000..9b18cf0f Binary files /dev/null and b/public/images/os/amazon-os.png differ diff --git a/public/images/os/android-os.png b/public/images/os/android-os.png new file mode 100644 index 00000000..fc6509b3 Binary files /dev/null and b/public/images/os/android-os.png differ diff --git a/public/images/os/beos.png b/public/images/os/beos.png new file mode 100644 index 00000000..6bc4a8a5 Binary files /dev/null and b/public/images/os/beos.png differ diff --git a/public/images/os/blackberry-os.png b/public/images/os/blackberry-os.png new file mode 100644 index 00000000..c77db525 Binary files /dev/null and b/public/images/os/blackberry-os.png differ diff --git a/public/images/os/chrome-os.png b/public/images/os/chrome-os.png new file mode 100644 index 00000000..ae008601 Binary files /dev/null and b/public/images/os/chrome-os.png differ diff --git a/public/images/os/ios.png b/public/images/os/ios.png new file mode 100644 index 00000000..1c129ae8 Binary files /dev/null and b/public/images/os/ios.png differ diff --git a/public/images/os/linux.png b/public/images/os/linux.png new file mode 100644 index 00000000..ce8fba38 Binary files /dev/null and b/public/images/os/linux.png differ diff --git a/public/images/os/mac-os.png b/public/images/os/mac-os.png new file mode 100644 index 00000000..1972abe7 Binary files /dev/null and b/public/images/os/mac-os.png differ diff --git a/public/images/os/open-bsd.png b/public/images/os/open-bsd.png new file mode 100644 index 00000000..806887e8 Binary files /dev/null and b/public/images/os/open-bsd.png differ diff --git a/public/images/os/os-2.png b/public/images/os/os-2.png new file mode 100644 index 00000000..5f88105d Binary files /dev/null and b/public/images/os/os-2.png differ diff --git a/public/images/os/qnx.png b/public/images/os/qnx.png new file mode 100644 index 00000000..59d9a44c Binary files /dev/null and b/public/images/os/qnx.png differ diff --git a/public/images/os/sun-os.png b/public/images/os/sun-os.png new file mode 100644 index 00000000..c19f0eb3 Binary files /dev/null and b/public/images/os/sun-os.png differ diff --git a/public/images/os/unknown.png b/public/images/os/unknown.png new file mode 100644 index 00000000..52058026 Binary files /dev/null and b/public/images/os/unknown.png differ diff --git a/public/images/os/windows-10.png b/public/images/os/windows-10.png new file mode 100644 index 00000000..4effcd2b Binary files /dev/null and b/public/images/os/windows-10.png differ diff --git a/public/images/os/windows-11.png b/public/images/os/windows-11.png new file mode 100644 index 00000000..4effcd2b Binary files /dev/null and b/public/images/os/windows-11.png differ diff --git a/public/images/os/windows-2000.png b/public/images/os/windows-2000.png new file mode 100644 index 00000000..3bccae3f Binary files /dev/null and b/public/images/os/windows-2000.png differ diff --git a/public/images/os/windows-3-11.png b/public/images/os/windows-3-11.png new file mode 100644 index 00000000..3bccae3f Binary files /dev/null and b/public/images/os/windows-3-11.png differ diff --git a/public/images/os/windows-7.png b/public/images/os/windows-7.png new file mode 100644 index 00000000..cd2db79e Binary files /dev/null and b/public/images/os/windows-7.png differ diff --git a/public/images/os/windows-8-1.png b/public/images/os/windows-8-1.png new file mode 100644 index 00000000..3ce98aaa Binary files /dev/null and b/public/images/os/windows-8-1.png differ diff --git a/public/images/os/windows-8.png b/public/images/os/windows-8.png new file mode 100644 index 00000000..3ce98aaa Binary files /dev/null and b/public/images/os/windows-8.png differ diff --git a/public/images/os/windows-95.png b/public/images/os/windows-95.png new file mode 100644 index 00000000..3bccae3f Binary files /dev/null and b/public/images/os/windows-95.png differ diff --git a/public/images/os/windows-98.png b/public/images/os/windows-98.png new file mode 100644 index 00000000..3bccae3f Binary files /dev/null and b/public/images/os/windows-98.png differ diff --git a/public/images/os/windows-me.png b/public/images/os/windows-me.png new file mode 100644 index 00000000..cd2db79e Binary files /dev/null and b/public/images/os/windows-me.png differ diff --git a/public/images/os/windows-server-2003.png b/public/images/os/windows-server-2003.png new file mode 100644 index 00000000..cd2db79e Binary files /dev/null and b/public/images/os/windows-server-2003.png differ diff --git a/public/images/os/windows-vista.png b/public/images/os/windows-vista.png new file mode 100644 index 00000000..cd2db79e Binary files /dev/null and b/public/images/os/windows-vista.png differ diff --git a/public/images/os/windows-xp.png b/public/images/os/windows-xp.png new file mode 100644 index 00000000..cd2db79e Binary files /dev/null and b/public/images/os/windows-xp.png differ diff --git a/queries/admin/report.ts b/queries/admin/report.ts index 506902f5..6b557a7a 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.ReportWhereInput): Promise { return prisma.client.report.findMany({ - where: { - userId, - }, + where, }); } diff --git a/queries/analytics/eventData/getEventData.ts b/queries/analytics/eventData/getEventData.ts index 8a3b2927..1190e191 100644 --- a/queries/analytics/eventData/getEventData.ts +++ b/queries/analytics/eventData/getEventData.ts @@ -1,28 +1,12 @@ +import prisma from 'lib/prisma'; import clickhouse from 'lib/clickhouse'; 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; - }, - ]; -} - 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,70 +14,81 @@ 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, + string_value as value, + 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, string_value + order by 3 desc, 2 desc, 1 asc + limit 100 + `, + [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 100 + `, + [websiteId, resetDate, startDate, endDate] as any, + ); } -async function clickhouseQuery( - websiteId: string, - startDate: Date, - endDate: Date, - criteria: EventDataCriteria, -) { - const { fields } = 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 }) => { - if (!obj[name]) { - obj[name] = { - columns: ['event_key as field', `count(*) as total`, `${type}_value as value`], - groups: ['event_key', `${type}_value`], - }; - } - 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, + string_value as value, + 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 20 + group by event_key, string_value + order by 3 desc, 2 desc, 1 asc + limit 100 `, - 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 100 + `, + { websiteId }, + ); } diff --git a/yarn.lock b/yarn.lock index fc94a0f7..7dafda26 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1072,6 +1072,21 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@csstools/css-parser-algorithms@^2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.3.0.tgz#0cc3a656dc2d638370ecf6f98358973bfbd00141" + integrity sha512-dTKSIHHWc0zPvcS5cqGP+/TPFUJB0ekJ9dGKvMAFoNuBFhDPBt9OMGNZiIA5vTiNdGHHBeScYPXIGBMnVOahsA== + +"@csstools/css-tokenizer@^2.1.1": + version "2.1.1" + resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.1.1.tgz#07ae11a0a06365d7ec686549db7b729bc036528e" + integrity sha512-GbrTj2Z8MCTUv+52GE0RbFGM527xuXZ0Xa5g0Z+YN573uveS4G0qi6WNOMyz3yrFM/jaILTTwJ0+umx81EzqfA== + +"@csstools/media-query-list-parser@^2.1.2": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.2.tgz#6ef642b728d30c1009bfbba3211c7e4c11302728" + integrity sha512-M8cFGGwl866o6++vIY7j1AKuq9v57cf+dGepScwCcbut9ypJNr4Cj+LLTWligYUZ0uyhEoJDKt5lvyBfh2L3ZQ== + "@csstools/postcss-cascade-layers@^1.1.1": version "1.1.1" resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-1.1.1.tgz" @@ -1183,6 +1198,11 @@ resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-2.0.2.tgz" integrity sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg== +"@csstools/selector-specificity@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@csstools/selector-specificity/-/selector-specificity-3.0.0.tgz#798622546b63847e82389e473fd67f2707d82247" + integrity sha512-hBI9tfBtuPIi885ZsZ32IMEU/5nlZH/KOVYJCOh7gyMxaVLGmLedYqFN6Ui1LXkI8JlC8IsuC0rF0btcRZKd5g== + "@esbuild/android-arm64@0.17.19": version "0.17.19" resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" @@ -2367,7 +2387,7 @@ resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz" integrity sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA== -"@types/minimist@^1.2.0": +"@types/minimist@^1.2.0", "@types/minimist@^1.2.2": version "1.2.2" resolved "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== @@ -3110,12 +3130,22 @@ camelcase-keys@^6.2.2: map-obj "^4.0.0" quick-lru "^4.0.1" +camelcase-keys@^7.0.0: + version "7.0.2" + resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-7.0.2.tgz#d048d8c69448745bb0de6fc4c1c52a30dfbe7252" + integrity sha512-Rjs1H+A9R+Ig+4E/9oyB66UC5Mj9Xq3N//vcLf2WzgdTi/3gUu3Z9KoqmlrEG4VuuLK8wJHofxzdQXz/knhiYg== + dependencies: + camelcase "^6.3.0" + map-obj "^4.1.0" + quick-lru "^5.1.1" + type-fest "^1.2.1" + camelcase@^5.0.0, camelcase@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz" integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== -camelcase@^6.2.0: +camelcase@^6.2.0, camelcase@^6.3.0: version "6.3.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== @@ -3401,7 +3431,7 @@ cors@^2.8.5: object-assign "^4" vary "^1" -cosmiconfig@^7.0.1, cosmiconfig@^7.1.0: +cosmiconfig@^7.0.1: version "7.1.0" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz" integrity sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA== @@ -3422,6 +3452,16 @@ cosmiconfig@^8.1.3: parse-json "^5.0.0" path-type "^4.0.0" +cosmiconfig@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-8.2.0.tgz#f7d17c56a590856cd1e7cee98734dca272b0d8fd" + integrity sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ== + dependencies: + import-fresh "^3.2.1" + js-yaml "^4.1.0" + parse-json "^5.0.0" + path-type "^4.0.0" + create-require@^1.1.0: version "1.1.1" resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz" @@ -3520,7 +3560,7 @@ css-tree@^1.1.2, css-tree@^1.1.3: mdn-data "2.0.14" source-map "^0.6.1" -css-tree@^2.2.1: +css-tree@^2.2.1, css-tree@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz" integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw== @@ -3778,6 +3818,11 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== +decamelize@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-5.0.1.tgz#db11a92e58c741ef339fb0a2868d8a06a9a7b1e9" + integrity sha512-VfxadyCECXgQlkoEAjeghAr5gY3Hf+IKjKb+X8tGVDtveCjN+USwprd2q3QXBR9T1+x2DG0XZF5/w+7HAtSaXA== + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz" @@ -4470,7 +4515,7 @@ fast-equals@^3.0.1: resolved "https://registry.npmjs.org/fast-equals/-/fast-equals-3.0.3.tgz" integrity sha512-NCe8qxnZFARSHGztGMZOO/PC1qa5MIFB5Hp66WdzbCRAz8U8US3bx1UTgLS49efBQPcUtO9gf5oVEY8o7y/7Kg== -fast-glob@^3.0.3, fast-glob@^3.2.12, fast-glob@^3.2.7, fast-glob@^3.2.9: +fast-glob@^3.0.3, fast-glob@^3.2.7, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz" integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== @@ -4481,6 +4526,17 @@ fast-glob@^3.0.3, fast-glob@^3.2.12, fast-glob@^3.2.7, fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.0.tgz#7c40cb491e1e2ed5664749e87bfb516dbe8727c0" + integrity sha512-ChDuvbOypPuNjO8yIDf36x7BlZX1smcUMTTcyoIjycexOxd6DFsKsg21qVBzEmr3G7fUKIRy2/psii+CIUt7FA== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" @@ -4996,10 +5052,10 @@ hosted-git-info@^4.0.1: dependencies: lru-cache "^6.0.0" -html-tags@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz" - integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== +html-tags@^3.3.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.3.1.tgz#a04026a18c882e4bba8a01a3d39cfe465d40b5ce" + integrity sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ== http-shutdown@^1.2.2: version "1.2.2" @@ -5040,7 +5096,7 @@ ieee754@^1.1.13: resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.1: +ignore@^5.1.1, ignore@^5.2.0, ignore@^5.2.4: version "5.2.4" resolved "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz" integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== @@ -5092,6 +5148,11 @@ indent-string@^4.0.0: resolved "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz" integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== +indent-string@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-5.0.0.tgz#4fd2980fccaf8622d14c64d694f4cf33c81951a5" + integrity sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz" @@ -5649,10 +5710,10 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -known-css-properties@^0.26.0: - version "0.26.0" - resolved "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.26.0.tgz" - integrity sha512-5FZRzrZzNTBruuurWpvZnvP9pum+fe0HcK8z/ooo+U+Hmp4vtbyp1/QDsqmufirXy4egGzbaH/y2uCZf+6W5Kg== +known-css-properties@^0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.27.0.tgz#82a9358dda5fe7f7bd12b5e7142c0a205393c0c5" + integrity sha512-uMCj6+hZYDoffuvAJjFAPz56E9uoowFHmTkqRtRq5WyC5Q6Cu/fTZKNQpX/RbzChBYLLl3lo8CjFZBAZXq9qFg== language-subtag-registry@^0.3.20: version "0.3.22" @@ -5926,7 +5987,7 @@ map-obj@^1.0.0: resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz" integrity sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg== -map-obj@^4.0.0: +map-obj@^4.0.0, map-obj@^4.1.0: version "4.3.0" resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz" integrity sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ== @@ -5969,6 +6030,24 @@ memorystream@^0.3.1: resolved "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz" integrity sha1-htcJCzDORV1j+64S3aUaR93K+bI= +meow@^10.1.5: + version "10.1.5" + resolved "https://registry.yarnpkg.com/meow/-/meow-10.1.5.tgz#be52a1d87b5f5698602b0f32875ee5940904aa7f" + integrity sha512-/d+PQ4GKmGvM9Bee/DPa8z3mXs/pkvJE2KEThngVNOqtmljC6K7NMPxtc2JeZYTmpWb9k/TmxjeL18ez3h7vCw== + dependencies: + "@types/minimist" "^1.2.2" + camelcase-keys "^7.0.0" + decamelize "^5.0.0" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.2" + read-pkg-up "^8.0.0" + redent "^4.0.0" + trim-newlines "^4.0.2" + type-fest "^1.2.2" + yargs-parser "^20.2.9" + meow@^6.1.0: version "6.1.1" resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz" @@ -5986,24 +6065,6 @@ meow@^6.1.0: type-fest "^0.13.1" yargs-parser "^18.1.3" -meow@^9.0.0: - version "9.0.0" - resolved "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz" - integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== - dependencies: - "@types/minimist" "^1.2.0" - camelcase-keys "^6.2.2" - decamelize "^1.2.0" - decamelize-keys "^1.1.0" - hard-rejection "^2.1.0" - minimist-options "4.1.0" - normalize-package-data "^3.0.0" - read-pkg-up "^7.0.1" - redent "^3.0.0" - trim-newlines "^3.0.0" - type-fest "^0.18.0" - yargs-parser "^20.2.3" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz" @@ -6049,7 +6110,7 @@ mimic-response@^3.1.0: resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -min-indent@^1.0.0: +min-indent@^1.0.0, min-indent@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz" integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== @@ -6293,9 +6354,9 @@ normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" -normalize-package-data@^3.0.0: +normalize-package-data@^3.0.2: version "3.0.3" - resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.3.tgz#dbcc3e2da59509a0983422884cd172eefdfa525e" integrity sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA== dependencies: hosted-git-info "^4.0.1" @@ -6544,7 +6605,7 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" -parse-json@^5.0.0: +parse-json@^5.0.0, parse-json@^5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz" integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== @@ -7149,7 +7210,7 @@ postcss-selector-not@^6.0.1: dependencies: postcss-selector-parser "^6.0.10" -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: +postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.2, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.6, postcss-selector-parser@^6.0.9: version "6.0.11" resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz" integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== @@ -7157,6 +7218,14 @@ postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-select cssesc "^3.0.0" util-deprecate "^1.0.2" +postcss-selector-parser@^6.0.13: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz#d05d8d76b1e8e173257ef9d60b706a8e5e99bf1b" + integrity sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + postcss-svgo@^5.1.0: version "5.1.0" resolved "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-5.1.0.tgz" @@ -7186,7 +7255,7 @@ postcss@8.4.14: picocolors "^1.0.0" source-map-js "^1.0.2" -postcss@^8.1.10, postcss@^8.4.19, postcss@^8.4.21: +postcss@^8.1.10, postcss@^8.4.21: version "8.4.24" resolved "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz" integrity sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg== @@ -7195,6 +7264,15 @@ postcss@^8.1.10, postcss@^8.4.19, postcss@^8.4.21: picocolors "^1.0.0" source-map-js "^1.0.2" +postcss@^8.4.24: + version "8.4.25" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.25.tgz#4a133f5e379eda7f61e906c3b1aaa9b81292726f" + integrity sha512-7taJ/8t2av0Z+sQEvNzCkpDynl0tX3uJMCODi6nT3PfASC7dYCWV9aQ+uiCf+KBD4SEFcu+GvJdGdwzQ6OSjCw== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + prebuild-install@^7.1.1: version "7.1.1" resolved "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz" @@ -7317,6 +7395,11 @@ quick-lru@^4.0.1: resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz" integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g== +quick-lru@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" + integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== + radix3@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/radix3/-/radix3-1.0.1.tgz" @@ -7344,10 +7427,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" @@ -7499,6 +7582,15 @@ read-pkg-up@^7.0.1: read-pkg "^5.2.0" type-fest "^0.8.1" +read-pkg-up@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-8.0.0.tgz#72f595b65e66110f43b052dd9af4de6b10534670" + integrity sha512-snVCqPczksT0HS2EC+SxUndvSzn6LRCwpfSvLrIfR5BKDQQZMaI6jPRC9dYvYFDRAuFEAnkwww8kBBNE/3VvzQ== + dependencies: + find-up "^5.0.0" + read-pkg "^6.0.0" + type-fest "^1.0.1" + read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz" @@ -7518,6 +7610,16 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" +read-pkg@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-6.0.0.tgz#a67a7d6a1c2b0c3cd6aa2ea521f40c458a4a504c" + integrity sha512-X1Fu3dPuk/8ZLsMhEj5f4wFAF0DWoK7qhGJvgaijocXxBmSToKfbFtqbxMO7bVjNA1dmE5huAzjXj/ey86iw9Q== + dependencies: + "@types/normalize-package-data" "^2.4.0" + normalize-package-data "^3.0.2" + parse-json "^5.2.0" + type-fest "^1.0.1" + readable-stream@^3.1.1, readable-stream@^3.4.0: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" @@ -7542,6 +7644,14 @@ redent@^3.0.0: indent-string "^4.0.0" strip-indent "^3.0.0" +redent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/redent/-/redent-4.0.0.tgz#0c0ba7caabb24257ab3bb7a4fd95dd1d5c5681f9" + integrity sha512-tYkDkVVtYkSVhuQ4zBgfvciymHaeuel+zFKXShfDnFP5SyVEP7qo70Rf1jTOTCx3vGNAbnEi/xFkcfQVMIBWag== + dependencies: + indent-string "^5.0.0" + strip-indent "^4.0.0" + redis-errors@^1.0.0, redis-errors@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz" @@ -8004,11 +8114,16 @@ side-channel@^1.0.4: get-intrinsic "^1.0.2" object-inspect "^1.9.0" -signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: +signal-exit@^3.0.2, signal-exit@^3.0.3: version "3.0.7" resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz" integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== +signal-exit@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.0.2.tgz#ff55bb1d9ff2114c13b400688fa544ac63c36967" + integrity sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q== + simple-concat@^1.0.0: version "1.0.1" resolved "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz" @@ -8264,6 +8379,13 @@ strip-indent@^3.0.0: dependencies: min-indent "^1.0.0" +strip-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-4.0.0.tgz#b41379433dd06f5eae805e21d631e07ee670d853" + integrity sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA== + dependencies: + min-indent "^1.0.1" + strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: version "3.1.1" resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz" @@ -8327,49 +8449,51 @@ stylelint-scss@^4.3.0: postcss-selector-parser "^6.0.6" postcss-value-parser "^4.1.0" -stylelint@^14.16.1: - version "14.16.1" - resolved "https://registry.npmjs.org/stylelint/-/stylelint-14.16.1.tgz" - integrity sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A== +stylelint@^15.10.1: + version "15.10.1" + resolved "https://registry.yarnpkg.com/stylelint/-/stylelint-15.10.1.tgz#93f189958687e330c106b010cbec0c41dcae506d" + integrity sha512-CYkzYrCFfA/gnOR+u9kJ1PpzwG10WLVnoxHDuBA/JiwGqdM9+yx9+ou6SE/y9YHtfv1mcLo06fdadHTOx4gBZQ== dependencies: - "@csstools/selector-specificity" "^2.0.2" + "@csstools/css-parser-algorithms" "^2.3.0" + "@csstools/css-tokenizer" "^2.1.1" + "@csstools/media-query-list-parser" "^2.1.2" + "@csstools/selector-specificity" "^3.0.0" balanced-match "^2.0.0" colord "^2.9.3" - cosmiconfig "^7.1.0" + cosmiconfig "^8.2.0" css-functions-list "^3.1.0" + css-tree "^2.3.1" debug "^4.3.4" - fast-glob "^3.2.12" + fast-glob "^3.3.0" fastest-levenshtein "^1.0.16" file-entry-cache "^6.0.1" global-modules "^2.0.0" globby "^11.1.0" globjoin "^0.1.4" - html-tags "^3.2.0" - ignore "^5.2.1" + html-tags "^3.3.1" + ignore "^5.2.4" import-lazy "^4.0.0" imurmurhash "^0.1.4" is-plain-object "^5.0.0" - known-css-properties "^0.26.0" + known-css-properties "^0.27.0" mathml-tag-names "^2.1.3" - meow "^9.0.0" + meow "^10.1.5" micromatch "^4.0.5" normalize-path "^3.0.0" picocolors "^1.0.0" - postcss "^8.4.19" - postcss-media-query-parser "^0.2.3" + postcss "^8.4.24" postcss-resolve-nested-selector "^0.1.1" postcss-safe-parser "^6.0.0" - postcss-selector-parser "^6.0.11" + postcss-selector-parser "^6.0.13" postcss-value-parser "^4.2.0" resolve-from "^5.0.0" string-width "^4.2.3" strip-ansi "^6.0.1" style-search "^0.1.0" - supports-hyperlinks "^2.3.0" + supports-hyperlinks "^3.0.0" svg-tags "^1.0.0" table "^6.8.1" - v8-compile-cache "^2.3.0" - write-file-atomic "^4.0.2" + write-file-atomic "^5.0.1" supports-color@8.1.1: version "8.1.1" @@ -8392,10 +8516,10 @@ supports-color@^7.0.0, supports-color@^7.1.0: dependencies: has-flag "^4.0.0" -supports-hyperlinks@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz" - integrity sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA== +supports-hyperlinks@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz#c711352a5c89070779b4dad54c05a2f14b15c94b" + integrity sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA== dependencies: has-flag "^4.0.0" supports-color "^7.0.0" @@ -8582,6 +8706,11 @@ trim-newlines@^3.0.0: resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz" integrity sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw== +trim-newlines@^4.0.2: + version "4.1.1" + resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-4.1.1.tgz#28c88deb50ed10c7ba6dc2474421904a00139125" + integrity sha512-jRKj0n0jXWo6kh62nA5TEh3+4igKDXLvzBJcPpiizP7oOolUrYIxmVBG9TOtHYFHoddUk6YvAkGeGoSVTXfQXQ== + ts-node@^10.9.1: version "10.9.1" resolved "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz" @@ -8662,11 +8791,6 @@ type-fest@^0.13.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.18.0: - version "0.18.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz" - integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== - type-fest@^0.20.2: version "0.20.2" resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz" @@ -8687,6 +8811,11 @@ type-fest@^0.8.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^1.0.1, type-fest@^1.2.1, type-fest@^1.2.2: + version "1.4.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1" + integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA== + typed-array-length@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz" @@ -8858,11 +8987,6 @@ v8-compile-cache-lib@^3.0.1: resolved "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz" integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== -v8-compile-cache@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz" - integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== - validate-npm-package-license@^3.0.1: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz" @@ -8989,13 +9113,13 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-file-atomic@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== +write-file-atomic@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-5.0.1.tgz#68df4717c55c6fa4281a7860b4c2ba0a6d2b11e7" + integrity sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw== dependencies: imurmurhash "^0.1.4" - signal-exit "^3.0.7" + signal-exit "^4.0.1" write-json-file@^4.3.0: version "4.3.0" @@ -9040,9 +9164,9 @@ yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.3: +yargs-parser@^20.2.9: version "20.2.9" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== yn@3.1.1: