diff --git a/.gitignore b/.gitignore index 54410324..566dc61b 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ # production /build -/public/umami.js +/public/script.js /public/geo # misc diff --git a/components/common/HoverTooltip.js b/components/common/HoverTooltip.js new file mode 100644 index 00000000..3b885b1d --- /dev/null +++ b/components/common/HoverTooltip.js @@ -0,0 +1,25 @@ +import { useEffect, useState } from 'react'; +import { Tooltip } from 'react-basics'; +import styles from './HoverTooltip.module.css'; + +export default function HoverTooltip({ tooltip }) { + const [position, setPosition] = useState({ x: -1000, y: -1000 }); + + useEffect(() => { + const handler = e => { + setPosition({ x: e.clientX, y: e.clientY }); + }; + + document.addEventListener('mousemove', handler); + + return () => { + document.removeEventListener('mousemove', handler); + }; + }, []); + + return ( +
+ +
+ ); +} diff --git a/components/metrics/ChartTooltip.module.css b/components/common/HoverTooltip.module.css similarity index 95% rename from components/metrics/ChartTooltip.module.css rename to components/common/HoverTooltip.module.css index 2d7ba958..519e303d 100644 --- a/components/metrics/ChartTooltip.module.css +++ b/components/common/HoverTooltip.module.css @@ -3,7 +3,7 @@ } .tooltip { - color: var(--msgColor); + position: fixed; pointer-events: none; z-index: 1; } diff --git a/components/common/WorldMap.js b/components/common/WorldMap.js index 8afd84e0..b774702b 100644 --- a/components/common/WorldMap.js +++ b/components/common/WorldMap.js @@ -1,6 +1,5 @@ import { useState, useMemo } from 'react'; import { useRouter } from 'next/router'; -import ReactTooltip from 'react-tooltip'; import { ComposableMap, Geographies, Geography, ZoomableGroup } from 'react-simple-maps'; import classNames from 'classnames'; import { colord } from 'colord'; @@ -9,6 +8,8 @@ import { ISO_COUNTRIES, THEME_COLORS, MAP_FILE } from 'lib/constants'; import styles from './WorldMap.module.css'; import useCountryNames from 'hooks/useCountryNames'; import useLocale from 'hooks/useLocale'; +import HoverTooltip from './HoverTooltip'; +import { formatLongNumber } from '../../lib/format'; function WorldMap({ data, className }) { const { basePath } = useRouter(); @@ -46,7 +47,7 @@ function WorldMap({ data, className }) { function handleHover(code) { if (code === 'AQ') return; const country = data?.find(({ x }) => x === code); - setTooltip(`${countryNames[code]}: ${country?.y || 0} visitors`); + setTooltip(`${countryNames[code]}: ${formatLongNumber(country?.y || 0)} visitors`); } return ( @@ -83,7 +84,7 @@ function WorldMap({ data, className }) { - {tooltip} + {tooltip && } ); } diff --git a/components/layout/Header.module.css b/components/layout/Header.module.css index c896e967..e796b6f0 100644 --- a/components/layout/Header.module.css +++ b/components/layout/Header.module.css @@ -12,7 +12,7 @@ gap: 10px; font-size: var(--font-size-lg); font-weight: 700; - color: var(--font-color100); + color: var(--font-color100) !important; } .buttons { diff --git a/components/metrics/BarChart.js b/components/metrics/BarChart.js index f70e012e..65881e28 100644 --- a/components/metrics/BarChart.js +++ b/components/metrics/BarChart.js @@ -1,224 +1,185 @@ -import { useState, useRef, useEffect } from 'react'; +import { useState, useRef, useEffect, useMemo, useCallback } from 'react'; +import { StatusLight } from 'react-basics'; import classNames from 'classnames'; -import ChartJS from 'chart.js'; +import Chart from 'chart.js/auto'; +import HoverTooltip from 'components/common/HoverTooltip'; import Legend from 'components/metrics/Legend'; import { formatLongNumber } from 'lib/format'; import { dateFormat } from 'lib/date'; import useLocale from 'hooks/useLocale'; import useTheme from 'hooks/useTheme'; -import useForceUpdate from 'hooks/useForceUpdate'; import { DEFAULT_ANIMATION_DURATION, THEME_COLORS } from 'lib/constants'; import styles from './BarChart.module.css'; -import ChartTooltip from './ChartTooltip'; export default function BarChart({ - chartId, datasets, unit, - records, animationDuration = DEFAULT_ANIMATION_DURATION, - className, stacked = false, loading = false, onCreate = () => {}, onUpdate = () => {}, + className, }) { const canvas = useRef(); - const chart = useRef(); + const chart = useRef(null); const [tooltip, setTooltip] = useState(null); const { locale } = useLocale(); const [theme] = useTheme(); - const forceUpdate = useForceUpdate(); - const colors = { - text: THEME_COLORS[theme].gray700, - line: THEME_COLORS[theme].gray200, - zeroLine: THEME_COLORS[theme].gray500, + const colors = useMemo( + () => ({ + text: THEME_COLORS[theme].gray700, + line: THEME_COLORS[theme].gray200, + }), + [theme], + ); + + const renderYLabel = label => { + return +label > 1000 ? formatLongNumber(label) : label; }; - function renderXLabel(label, index, values) { - if (loading) return ''; - const d = new Date(values[index].value); - const sw = canvas.current.width / window.devicePixelRatio; + const renderTooltip = useCallback( + model => { + const { opacity, labelColors, dataPoints } = model.tooltip; - switch (unit) { - case 'minute': - return index % 2 === 0 ? dateFormat(d, 'H:mm', locale) : ''; - case 'hour': - return dateFormat(d, 'p', locale); - case 'day': - if (records > 25) { - if (sw <= 275) { - return index % 10 === 0 ? dateFormat(d, 'M/d', locale) : ''; - } - if (sw <= 550) { - return index % 5 === 0 ? dateFormat(d, 'M/d', locale) : ''; - } - if (sw <= 700) { - return index % 2 === 0 ? dateFormat(d, 'M/d', locale) : ''; - } - return dateFormat(d, 'MMM d', locale); - } - if (sw <= 375) { - return index % 2 === 0 ? dateFormat(d, 'MMM d', locale) : ''; - } - if (sw <= 425) { - return dateFormat(d, 'MMM d', locale); - } - return dateFormat(d, 'EEE M/d', locale); - case 'month': - if (sw <= 330) { - return index % 2 === 0 ? dateFormat(d, 'MMM', locale) : ''; - } - return dateFormat(d, 'MMM', locale); - default: - return label; - } - } + if (!dataPoints?.length || !opacity) { + setTooltip(null); + return; + } - function renderYLabel(label) { - return +label > 1000 ? formatLongNumber(label) : label; - } + const formats = { + millisecond: 'T', + second: 'pp', + minute: 'p', + hour: 'h aaa', + day: 'PPPP', + week: 'PPPP', + month: 'LLLL yyyy', + quarter: 'qqq', + year: 'yyyy', + }; - function renderTooltip(model) { - const { opacity, title, body, labelColors } = model; + setTooltip( +
+
{dateFormat(new Date(dataPoints[0].raw.x), formats[unit], locale)}
+
+ +
+ {formatLongNumber(dataPoints[0].raw.y)} {dataPoints[0].dataset.label} +
+
+
+
, + ); + }, + [unit], + ); - if (!opacity || !title) { - setTooltip(null); - return; - } - - const [label, value] = body[0].lines[0].split(':'); - - setTooltip({ - title: dateFormat(new Date(+title[0]), getTooltipFormat(unit), locale), - value, - label, - labelColor: labelColors[0].backgroundColor, - }); - } - - function getTooltipFormat(unit) { - switch (unit) { - case 'hour': - return 'EEE p — PPP'; - default: - return 'PPPP'; - } - } - - function createChart() { - const options = { + const getOptions = useCallback(() => { + return { + responsive: true, + maintainAspectRatio: false, animation: { duration: animationDuration, + resize: { + duration: 0, + }, + active: { + duration: 0, + }, }, - tooltips: { - enabled: false, - custom: renderTooltip, - }, - hover: { - animationDuration: 0, - }, - responsive: true, - responsiveAnimationDuration: 0, - maintainAspectRatio: false, - legend: { - display: false, + plugins: { + legend: { + display: false, + }, + tooltip: { + enabled: false, + external: renderTooltip, + }, }, scales: { - xAxes: [ - { - type: 'time', - distribution: 'series', - time: { - unit, - tooltipFormat: 'x', - }, - ticks: { - callback: renderXLabel, - minRotation: 0, - maxRotation: 0, - fontColor: colors.text, - autoSkipPadding: 1, - }, - gridLines: { - display: false, - }, - offset: true, - stacked: true, + x: { + type: 'time', + stacked: true, + time: { + unit, }, - ], - yAxes: [ - { - ticks: { - callback: renderYLabel, - beginAtZero: true, - fontColor: colors.text, - }, - gridLines: { - color: colors.line, - zeroLineColor: colors.zeroLine, - }, - stacked, + grid: { + display: false, }, - ], + border: { + color: colors.line, + }, + ticks: { + color: colors.text, + autoSkip: false, + maxRotation: 0, + }, + }, + y: { + type: 'linear', + min: 0, + beginAtZero: true, + stacked, + grid: { + color: colors.line, + }, + border: { + color: colors.line, + }, + ticks: { + color: colors.text, + callback: renderYLabel, + }, + }, }, }; + }, [animationDuration, renderTooltip, stacked, colors]); + + const createChart = () => { + Chart.defaults.font.family = 'Inter'; + + const options = getOptions(); onCreate(options); - chart.current = new ChartJS(canvas.current, { + chart.current = new Chart(canvas.current, { type: 'bar', data: { datasets, }, options, }); - } + }; - function updateChart() { - const { options } = chart.current; + const updateChart = () => { + setTooltip(null); - options.legend.labels.fontColor = colors.text; - options.scales.xAxes[0].time.unit = unit; - options.scales.xAxes[0].ticks.callback = renderXLabel; - options.scales.xAxes[0].ticks.fontColor = colors.text; - options.scales.yAxes[0].ticks.fontColor = colors.text; - options.scales.yAxes[0].ticks.precision = 0; - options.scales.yAxes[0].gridLines.color = colors.line; - options.scales.yAxes[0].gridLines.zeroLineColor = colors.zeroLine; - options.animation.duration = animationDuration; - options.tooltips.custom = renderTooltip; + chart.current.options = getOptions(); onUpdate(chart.current); chart.current.update(); - - forceUpdate(); - } + }; useEffect(() => { if (datasets) { if (!chart.current) { createChart(); } else { - setTooltip(null); updateChart(); } } - }, [datasets, unit, animationDuration, locale, theme]); + }, [datasets, unit, theme, animationDuration, locale, loading]); return ( <> -
+
- + {tooltip && } ); } diff --git a/components/metrics/BarChart.module.css b/components/metrics/BarChart.module.css index 593bff91..850d1ea7 100644 --- a/components/metrics/BarChart.module.css +++ b/components/metrics/BarChart.module.css @@ -1,10 +1,21 @@ .chart { position: relative; height: 400px; + overflow: hidden; +} + +.tooltip { + display: flex; + flex-direction: column; + gap: 10px; +} + +.tooltip .value { + text-transform: lowercase; } @media only screen and (max-width: 992px) { .chart { - height: 200px; + /*height: 200px;*/ } } diff --git a/components/metrics/ChartTooltip.js b/components/metrics/ChartTooltip.js deleted file mode 100644 index c409f462..00000000 --- a/components/metrics/ChartTooltip.js +++ /dev/null @@ -1,26 +0,0 @@ -import { StatusLight } from 'react-basics'; -import styles from './ChartTooltip.module.css'; -import ReactTooltip from 'react-tooltip'; - -export default function ChartTooltip({ chartId, tooltip }) { - if (!tooltip) { - return null; - } - - const { title, value, label, labelColor } = tooltip; - - return ( - -
-
-
{title}
-
- - {value} {label} - -
-
-
-
- ); -} diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js index db647c2f..6ccab9ab 100644 --- a/components/metrics/EventsChart.js +++ b/components/metrics/EventsChart.js @@ -33,12 +33,12 @@ export default function EventsChart({ websiteId, className, token }) { if (!data) return []; if (isLoading) return data; - const map = data.reduce((obj, { x, t, y }) => { + const map = data.reduce((obj, { x, y }) => { if (!obj[x]) { obj[x] = []; } - obj[x].push({ t, y }); + obj[x].push({ x, y }); return obj; }, {}); @@ -76,7 +76,6 @@ export default function EventsChart({ websiteId, className, token }) { return ( - +
handleCloseFilter(key)}> + + {`${key}`} = {`${safeDecodeURI(params[key])}`} + + + +
); })} diff --git a/components/metrics/FilterTags.module.css b/components/metrics/FilterTags.module.css index 50ae60a0..1c8458ac 100644 --- a/components/metrics/FilterTags.module.css +++ b/components/metrics/FilterTags.module.css @@ -1,11 +1,22 @@ .filters { display: flex; - justify-content: flex-start; - align-items: flex-start; + align-items: center; + gap: 10px; } .tag { - text-align: center; - margin-bottom: 10px; - margin-right: 20px; + display: flex; + flex-direction: row; + align-items: center; + gap: 10px; + font-size: var(--font-size-sm); + border: 1px solid var(--base600); + border-radius: var(--border-radius); + line-height: 30px; + padding: 0 8px; + cursor: pointer; +} + +.tag:hover { + background: var(--base75); } diff --git a/components/metrics/Legend.module.css b/components/metrics/Legend.module.css index e78bf609..b079e67f 100644 --- a/components/metrics/Legend.module.css +++ b/components/metrics/Legend.module.css @@ -2,13 +2,13 @@ display: flex; justify-content: center; flex-wrap: wrap; - margin-top: 10px; + padding: 10px 0; } .label { display: flex; align-items: center; - font-size: var(--font-size-xs); + font-size: var(--font-size-sm); cursor: pointer; } diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js index 258ea1e8..945f7796 100644 --- a/components/metrics/MetricsTable.js +++ b/components/metrics/MetricsTable.js @@ -40,7 +40,7 @@ export default function MetricsTable({ const { data, isLoading, isFetched, error } = useQuery( [ - 'websites:mnetrics', + 'websites:metrics', { websiteId, type, modified, url, referrer, os, browser, device, country }, ], () => diff --git a/components/metrics/PageviewsChart.js b/components/metrics/PageviewsChart.js index f040f2eb..47a085f0 100644 --- a/components/metrics/PageviewsChart.js +++ b/components/metrics/PageviewsChart.js @@ -25,12 +25,16 @@ export default function PageviewsChart({ const primaryColor = colord(THEME_COLORS[theme].primary); return { views: { - background: primaryColor.alpha(0.4).toRgbString(), - border: primaryColor.alpha(0.5).toRgbString(), + hoverBackgroundColor: primaryColor.alpha(0.7).toRgbString(), + backgroundColor: primaryColor.alpha(0.4).toRgbString(), + borderColor: primaryColor.alpha(0.7).toRgbString(), + hoverBorderColor: primaryColor.toRgbString(), }, visitors: { - background: primaryColor.alpha(0.6).toRgbString(), - border: primaryColor.alpha(0.7).toRgbString(), + hoverBackgroundColor: primaryColor.alpha(0.9).toRgbString(), + backgroundColor: primaryColor.alpha(0.6).toRgbString(), + borderColor: primaryColor.alpha(0.9).toRgbString(), + hoverBorderColor: primaryColor.toRgbString(), }, }; }, [theme]); @@ -50,30 +54,28 @@ export default function PageviewsChart({ return null; } + const datasets = [ + { + label: formatMessage(labels.uniqueVisitors), + data: data.sessions, + borderWidth: 1, + ...colors.visitors, + }, + { + label: formatMessage(labels.pageViews), + data: data.pageviews, + borderWidth: 1, + ...colors.views, + }, + ]; + return (
{ return referrer ? ( @@ -34,14 +16,12 @@ export default function ReferrersTable({ websiteId, showFilters, ...props }) { return ( <> - {showFilters && } diff --git a/components/metrics/WebsiteChart.module.css b/components/metrics/WebsiteChart.module.css index 05bfcbb8..3a3d4718 100644 --- a/components/metrics/WebsiteChart.module.css +++ b/components/metrics/WebsiteChart.module.css @@ -7,7 +7,7 @@ .chart { position: relative; - padding-bottom: 10px; + overflow: hidden; } .title { diff --git a/components/pages/realtime/RealtimeLog.js b/components/pages/realtime/RealtimeLog.js index 06b7537e..be7aeb8d 100644 --- a/components/pages/realtime/RealtimeLog.js +++ b/components/pages/realtime/RealtimeLog.js @@ -5,7 +5,7 @@ import { FixedSizeList } from 'react-window'; import firstBy from 'thenby'; import FilterButtons from 'components/common/FilterButtons'; import NoData from 'components/common/NoData'; -import { getDeviceMessage, labels, messages } from 'components/messages'; +import { labels, messages } from 'components/messages'; import useLocale from 'hooks/useLocale'; import useCountryNames from 'hooks/useCountryNames'; import { BROWSERS } from 'lib/constants'; @@ -102,7 +102,7 @@ export default function RealtimeLog({ data, websiteDomain }) { country: {countryNames[country] || formatMessage(labels.unknown)}, browser: {BROWSERS[browser]}, os: {os}, - device: {formatMessage(getDeviceMessage(device))}, + device: {formatMessage(labels[device] || labels.unknown)}, }} /> ); diff --git a/components/pages/settings/teams/TeamWebsitesTable.js b/components/pages/settings/teams/TeamWebsitesTable.js index a596204c..52141523 100644 --- a/components/pages/settings/teams/TeamWebsitesTable.js +++ b/components/pages/settings/teams/TeamWebsitesTable.js @@ -17,14 +17,14 @@ import { labels } from 'components/messages'; import useUser from 'hooks/useUser'; import useApi from 'hooks/useApi'; -export default function TeamWebsitesTable({ teamId, data = [], onSave }) { +export default function TeamWebsitesTable({ data = [], onSave }) { const { formatMessage } = useIntl(); const { user } = useUser(); const { del, useMutation } = useApi(); - const { mutate } = useMutation(data => del(`/teamWebsites/${data.teamWebsiteId}`)); + const { mutate } = useMutation(({ teamWebsiteId }) => del(`/teamWebsites/${teamWebsiteId}`)); const columns = [ - { name: 'name', label: formatMessage(labels.name), style: { flex: 2 } }, + { name: 'name', label: formatMessage(labels.name) }, { name: 'domain', label: formatMessage(labels.domain) }, { name: 'action', label: ' ' }, ]; diff --git a/db/clickhouse/schema.sql b/db/clickhouse/schema.sql index 5cac11ef..e8251194 100644 --- a/db/clickhouse/schema.sql +++ b/db/clickhouse/schema.sql @@ -19,8 +19,11 @@ CREATE TABLE event subdivision2 LowCardinality(String), city String, --pageview - url String, - referrer String, + url_path String, + url_query String, + referrer_path String, + referrer_query String, + referrer_domain String, page_title String, --event event_type UInt32, @@ -48,8 +51,11 @@ CREATE TABLE event_queue ( subdivision2 LowCardinality(String), city String, --pageview - url String, - referrer String, + url_path String, + url_query String, + referrer_path String, + referrer_query String, + referrer_domain String, page_title String, --event event_type UInt32, @@ -79,8 +85,11 @@ SELECT website_id, subdivision1, subdivision2, city, - url, - referrer, + url_path, + url_query, + referrer_path, + referrer_query, + referrer_domain, page_title, event_type, event_name, diff --git a/db/mysql/migrations/01_init/migration.sql b/db/mysql/migrations/01_init/migration.sql index a083d03a..eacbc38d 100644 --- a/db/mysql/migrations/01_init/migration.sql +++ b/db/mysql/migrations/01_init/migration.sql @@ -61,8 +61,11 @@ CREATE TABLE `website_event` ( `website_id` VARCHAR(36) NOT NULL, `session_id` VARCHAR(36) NOT NULL, `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), - `url` VARCHAR(500) NOT NULL, - `referrer` VARCHAR(500) NULL, + `url_path` VARCHAR(500) NOT NULL, + `url_query` VARCHAR(500) NULL, + `referrer_path` VARCHAR(500) NULL, + `referrer_query` VARCHAR(500) NULL, + `referrer_domain` VARCHAR(500) NULL, `page_title` VARCHAR(500) NULL, `event_type` INTEGER UNSIGNED NOT NULL DEFAULT 1, `event_name` VARCHAR(50) NULL, @@ -79,14 +82,12 @@ CREATE TABLE `website_event` ( CREATE TABLE `team` ( `team_id` VARCHAR(36) NOT NULL, `name` VARCHAR(50) NOT NULL, - `user_id` VARCHAR(36) NOT NULL, `access_code` VARCHAR(50) NULL, `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), `updated_at` TIMESTAMP(0) NULL, UNIQUE INDEX `team_team_id_key`(`team_id`), UNIQUE INDEX `team_access_code_key`(`access_code`), - INDEX `team_user_id_idx`(`user_id`), INDEX `team_access_code_idx`(`access_code`), PRIMARY KEY (`team_id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -110,13 +111,11 @@ CREATE TABLE `team_user` ( CREATE TABLE `team_website` ( `team_website_id` VARCHAR(36) NOT NULL, `team_id` VARCHAR(36) NOT NULL, - `user_id` VARCHAR(36) NOT NULL, `website_id` VARCHAR(36) NOT NULL, `created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0), UNIQUE INDEX `team_website_team_website_id_key`(`team_website_id`), INDEX `team_website_team_id_idx`(`team_id`), - INDEX `team_website_user_id_idx`(`user_id`), INDEX `team_website_website_id_idx`(`website_id`), PRIMARY KEY (`team_website_id`) ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma index a47ff428..1f182716 100644 --- a/db/mysql/schema.prisma +++ b/db/mysql/schema.prisma @@ -19,7 +19,6 @@ model User { teamUser TeamUser[] Website Website[] - teamWebsite TeamWebsite[] @@map("user") } @@ -65,15 +64,18 @@ model Website { } model WebsiteEvent { - id String @id() @map("event_id") @db.VarChar(36) - websiteId String @map("website_id") @db.VarChar(36) - sessionId String @map("session_id") @db.VarChar(36) - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) - url String @db.VarChar(500) - referrer String? @db.VarChar(500) - pageTitle String? @map("page_title") @db.VarChar(500) - eventType Int @default(1) @map("event_type") @db.UnsignedInt - eventName String? @map("event_name") @db.VarChar(50) + id String @id() @map("event_id") @db.VarChar(36) + websiteId String @map("website_id") @db.VarChar(36) + sessionId String @map("session_id") @db.VarChar(36) + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) + urlPath String @map("url_path") @db.VarChar(500) + urlQuery String? @map("url_query") @db.VarChar(500) + referrerPath String? @map("referrer_path") @db.VarChar(500) + referrerQuery String? @map("referrer_query") @db.VarChar(500) + referrerDomain String? @map("referrer_domain") @db.VarChar(500) + pageTitle String? @map("page_title") @db.VarChar(500) + eventType Int @default(1) @map("event_type") @db.UnsignedInt + eventName String? @map("event_name") @db.VarChar(50) @@index([createdAt]) @@index([sessionId]) @@ -86,7 +88,6 @@ model WebsiteEvent { model Team { id String @id() @unique() @map("team_id") @db.VarChar(36) name String @db.VarChar(50) - userId String @map("user_id") @db.VarChar(36) accessCode String? @unique @map("access_code") @db.VarChar(50) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) updatedAt DateTime? @map("updated_at") @db.Timestamp(0) @@ -94,7 +95,6 @@ model Team { teamUsers TeamUser[] teamWebsite TeamWebsite[] - @@index([userId]) @@index([accessCode]) @@map("team") } @@ -118,16 +118,13 @@ model TeamUser { model TeamWebsite { id String @id() @unique() @map("team_website_id") @db.VarChar(36) teamId String @map("team_id") @db.VarChar(36) - userId String @map("user_id") @db.VarChar(36) websiteId String @map("website_id") @db.VarChar(36) createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0) team Team @relation(fields: [teamId], references: [id]) - user User @relation(fields: [userId], references: [id]) website Website @relation(fields: [websiteId], references: [id]) @@index([teamId]) - @@index([userId]) @@index([websiteId]) @@map("team_website") } diff --git a/db/postgresql/migrations/07_remove_user_id/migration.sql b/db/postgresql/migrations/07_remove_user_id/migration.sql index 63122f49..ff92f545 100644 --- a/db/postgresql/migrations/07_remove_user_id/migration.sql +++ b/db/postgresql/migrations/07_remove_user_id/migration.sql @@ -15,5 +15,4 @@ DROP INDEX "team_website_user_id_idx"; ALTER TABLE "team" DROP COLUMN "user_id"; -- AlterTable -ALTER TABLE "team_website" DROP COLUMN "user_id", -ADD COLUMN "userId" UUID; +ALTER TABLE "team_website" DROP COLUMN "user_id"; \ No newline at end of file diff --git a/db/postgresql/migrations/08_split_url_referrer/migration.sql b/db/postgresql/migrations/08_split_url_referrer/migration.sql new file mode 100644 index 00000000..7129d87a --- /dev/null +++ b/db/postgresql/migrations/08_split_url_referrer/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the column `referrer` on the `website_event` table. All the data in the column will be lost. + - You are about to drop the column `url` on the `website_event` table. All the data in the column will be lost. + - Added the required column `url_path` to the `website_event` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "website_event" DROP COLUMN "referrer", +DROP COLUMN "url", +ADD COLUMN "referrer_domain" VARCHAR(500), +ADD COLUMN "referrer_path" VARCHAR(500), +ADD COLUMN "referrer_query" VARCHAR(500), +ADD COLUMN "url_path" VARCHAR(500) NOT NULL, +ADD COLUMN "url_query" VARCHAR(500); diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma index be3be8f1..82b22c64 100644 --- a/db/postgresql/schema.prisma +++ b/db/postgresql/schema.prisma @@ -64,15 +64,18 @@ model Website { } model WebsiteEvent { - id String @id() @map("event_id") @db.Uuid - websiteId String @map("website_id") @db.Uuid - sessionId String @map("session_id") @db.Uuid - createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) - url String @db.VarChar(500) - referrer String? @db.VarChar(500) - pageTitle String? @map("page_title") @db.VarChar(500) - eventType Int @default(1) @map("event_type") @db.Integer - eventName String? @map("event_name") @db.VarChar(50) + id String @id() @map("event_id") @db.Uuid + websiteId String @map("website_id") @db.Uuid + sessionId String @map("session_id") @db.Uuid + createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6) + urlPath String @map("url_path") @db.VarChar(500) + urlQuery String? @map("url_query") @db.VarChar(500) + referrerPath String? @map("referrer_path") @db.VarChar(500) + referrerQuery String? @map("referrer_query") @db.VarChar(500) + referrerDomain String? @map("referrer_domain") @db.VarChar(500) + pageTitle String? @map("page_title") @db.VarChar(500) + eventType Int @default(1) @map("event_type") @db.Integer + eventName String? @map("event_name") @db.VarChar(50) @@index([createdAt]) @@index([sessionId]) diff --git a/hooks/useDateRange.js b/hooks/useDateRange.js index 16f3818c..a9896065 100644 --- a/hooks/useDateRange.js +++ b/hooks/useDateRange.js @@ -1,4 +1,3 @@ -import { parseISO } from 'date-fns'; import { parseDateRange } from 'lib/date'; import { setItem } from 'next-basics'; import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants'; diff --git a/lib/clickhouse.ts b/lib/clickhouse.ts index a3030eeb..073a92e7 100644 --- a/lib/clickhouse.ts +++ b/lib/clickhouse.ts @@ -74,6 +74,9 @@ function getFilterQuery(filters = {}, params = {}) { switch (key) { case 'url': + arr.push(`and url_path = {${key}:String}`); + params[key] = filter; + break; case 'pageTitle': case 'os': case 'browser': @@ -92,18 +95,20 @@ function getFilterQuery(filters = {}, params = {}) { break; case 'referrer': - arr.push(`and referrer ILIKE {${key}:String}`); - params[key] = `%${filter}`; + arr.push(`and referrer_domain= {${key}:String}`); + params[key] = filter; break; case 'domain': - arr.push(`and referrer NOT ILIKE {${key}:String}`); - arr.push(`and referrer NOT ILIKE '/%'`); + arr.push(`and referrer_domain NOT ILIKE {${key}:String}`); + arr.push(`and referrer_domain NOT ILIKE '/%'`); params[key] = `%://${filter}/%`; break; case 'query': - arr.push(`and url like '%?%'`); + arr.push(`and url_query= {${key}:String}`); + params[key] = filter; + break; } return arr; diff --git a/lib/date.js b/lib/date.js index b1273603..38e1cc61 100644 --- a/lib/date.js +++ b/lib/date.js @@ -181,18 +181,18 @@ export function getDateArray(data, startDate, endDate, unit) { const n = diff(endDate, startDate) + 1; function findData(t) { - const x = data.find(e => { - return normalize(getDateFromString(e.t)).getTime() === t.getTime(); + const d = data.find(({ x }) => { + return normalize(getDateFromString(x)).getTime() === t.getTime(); }); - return x?.y || 0; + return d?.y || 0; } for (let i = 0; i < n; i++) { const t = normalize(add(startDate, i)); const y = findData(t); - arr.push({ ...data[i], t, y }); + arr.push({ x: t, y }); } return arr; diff --git a/lib/filters.js b/lib/filters.js index 7e247220..9681a802 100644 --- a/lib/filters.js +++ b/lib/filters.js @@ -1,30 +1,10 @@ export const urlFilter = data => { - const isValidUrl = url => { - return url !== '' && url !== null; - }; - - const cleanUrl = url => { - try { - const { pathname } = new URL(url, location.origin); - - return pathname; - } catch { - return null; - } - }; - const map = data.reduce((obj, { x, y }) => { - if (!isValidUrl(x)) { - return obj; - } - - const url = cleanUrl(x); - - if (url) { - if (!obj[url]) { - obj[url] = y; + if (x) { + if (!obj[x]) { + obj[x] = y; } else { - obj[url] += y; + obj[x] += y; } } diff --git a/lib/prisma.ts b/lib/prisma.ts index 4461f044..20a5e4a6 100644 --- a/lib/prisma.ts +++ b/lib/prisma.ts @@ -74,6 +74,9 @@ function getFilterQuery(filters = {}, params = []): string { switch (key) { case 'url': + arr.push(`and url_path=$${params.length + 1}`); + params.push(decodeURIComponent(filter)); + break; case 'os': case 'pageTitle': case 'browser': @@ -92,18 +95,20 @@ function getFilterQuery(filters = {}, params = []): string { break; case 'referrer': - arr.push(`and referrer like $${params.length + 1}`); - params.push(`%${decodeURIComponent(filter)}%`); + arr.push(`and referrer_domain=$${params.length + 1}`); + params.push(decodeURIComponent(filter)); break; case 'domain': - arr.push(`and referrer not like $${params.length + 1}`); - arr.push(`and referrer not like '/%'`); + arr.push(`and referrer_domain not like $${params.length + 1}`); + arr.push(`and referrer_domain not like '/%'`); params.push(`%://${filter}/%`); break; case 'query': - arr.push(`and url like '%?%'`); + arr.push(`and url_query=$${params.length + 1}`); + params.push(decodeURIComponent(filter)); + break; } return arr; diff --git a/package.json b/package.json index 16af05ae..bd4b67ec 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,8 @@ "@umami/prisma-client": "^0.2.0", "@umami/redis-client": "^0.2.0", "chalk": "^4.1.1", - "chart.js": "^2.9.4", + "chart.js": "^4.2.1", + "chartjs-adapter-date-fns": "^3.0.0", "classnames": "^2.3.1", "clickhouse": "^2.5.0", "colord": "^2.9.2", @@ -93,13 +94,12 @@ "node-fetch": "^3.2.8", "npm-run-all": "^4.1.5", "react": "^18.2.0", - "react-basics": "^0.71.0", + "react-basics": "^0.73.0", "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", "react-intl": "^5.24.7", "react-simple-maps": "^2.3.0", "react-spring": "^9.4.4", - "react-tooltip": "^4.2.21", "react-use-measure": "^2.0.4", "react-window": "^1.8.6", "request-ip": "^3.3.0", diff --git a/pages/_app.js b/pages/_app.js index ec14d084..ad9e71ac 100644 --- a/pages/_app.js +++ b/pages/_app.js @@ -10,6 +10,7 @@ import 'styles/locale.css'; import 'styles/index.css'; import '@fontsource/inter/400.css'; import '@fontsource/inter/700.css'; +import 'chartjs-adapter-date-fns'; import Script from 'next/script'; const client = new QueryClient({ diff --git a/pages/api/send.ts b/pages/api/send.ts index 639549f0..eab9fdfd 100644 --- a/pages/api/send.ts +++ b/pages/api/send.ts @@ -34,8 +34,7 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { const { type, payload } = getJsonBody(req); - const { referrer, eventName, eventData, pageTitle } = payload; - let { url } = payload; + const { url, referrer, eventName, eventData, pageTitle } = payload; // Validate eventData is JSON if (eventData && !(typeof eventData === 'object' && !Array.isArray(eventData))) { @@ -88,17 +87,41 @@ export default async (req: NextApiRequestCollect, res: NextApiResponse) => { const session = req.session; + let urlPath = url.split('?')[0]; + const urlQuery = url.split('?')[1]; + let referrerPath; + let referrerQuery; + let referrerDomain; + + try { + const newRef = new URL(referrer); + referrerPath = newRef.pathname; + referrerDomain = newRef.hostname; + referrerQuery = newRef.search.substring(1); + } catch { + referrerPath = referrer.split('?')[0]; + referrerQuery = referrer.split('?')[1]; + } + if (process.env.REMOVE_TRAILING_SLASH) { - url = url.replace(/\/$/, ''); + urlPath = urlPath.replace(/\/$/, ''); } if (type === 'pageview') { - await savePageView({ ...session, url, referrer, pageTitle }); + await savePageView({ + ...session, + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle, + }); } else if (type === 'event') { await saveEvent({ ...session, - url, - referrer, + urlPath, + urlQuery, pageTitle, eventName, eventData, diff --git a/pages/api/websites/[id]/metrics.ts b/pages/api/websites/[id]/metrics.ts index c547daee..ad57790e 100644 --- a/pages/api/websites/[id]/metrics.ts +++ b/pages/api/websites/[id]/metrics.ts @@ -6,7 +6,17 @@ import { NextApiResponse } from 'next'; import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics'; import { getPageviewMetrics, getSessionMetrics, getWebsite } from 'queries'; -const sessionColumns = ['browser', 'os', 'device', 'screen', 'country', 'language']; +const sessionColumns = [ + 'browser', + 'os', + 'device', + 'screen', + 'country', + 'language', + 'subdivision1', + 'subdivision2', + 'city', +]; const pageviewColumns = ['url', 'referrer', 'query', 'pageTitle']; function getTable(type) { @@ -26,12 +36,17 @@ function getTable(type) { } function getColumn(type) { - if (type === 'event') { - return 'event_name'; - } - if (type === 'query') { - return 'url'; + switch (type) { + case 'url': + return 'url_path'; + case 'referrer': + return 'referrer_domain'; + case 'event': + return 'event_name'; + case 'query': + return 'url_query'; } + return type; } diff --git a/queries/analytics/event/saveEvent.ts b/queries/analytics/event/saveEvent.ts index 27c5de00..88fd9f2c 100644 --- a/queries/analytics/event/saveEvent.ts +++ b/queries/analytics/event/saveEvent.ts @@ -9,8 +9,8 @@ import { saveEventData } from '../eventData/saveEventData'; export async function saveEvent(args: { id: string; websiteId: string; - url: string; - referrer?: string; + urlPath: string; + urlQuery?: string; pageTitle?: string; eventName?: string; eventData?: any; @@ -34,21 +34,21 @@ export async function saveEvent(args: { async function relationalQuery(data: { id: string; websiteId: string; - url: string; - referrer?: string; + urlPath: string; + urlQuery?: string; pageTitle?: string; eventName?: string; eventData?: any; }) { - const { websiteId, id: sessionId, url, eventName, referrer, pageTitle } = data; + const { websiteId, id: sessionId, urlPath, urlQuery, eventName, pageTitle } = data; return prisma.client.websiteEvent.create({ data: { id: uuid(), websiteId, sessionId, - url: url?.substring(0, URL_LENGTH), - referrer: referrer?.substring(0, URL_LENGTH), + urlPath: urlPath?.substring(0, URL_LENGTH), + urlQuery: urlQuery?.substring(0, URL_LENGTH), pageTitle: pageTitle, eventType: EVENT_TYPE.customEvent, eventName: eventName?.substring(0, EVENT_NAME_LENGTH), @@ -59,8 +59,8 @@ async function relationalQuery(data: { async function clickhouseQuery(data: { id: string; websiteId: string; - url: string; - referrer?: string; + urlPath: string; + urlQuery?: string; pageTitle?: string; eventName?: string; eventData?: any; @@ -78,7 +78,8 @@ async function clickhouseQuery(data: { const { websiteId, id: sessionId, - url, + urlPath, + urlQuery, pageTitle, eventName, eventData, @@ -101,7 +102,8 @@ async function clickhouseQuery(data: { subdivision1: subdivision1 ? subdivision1 : null, subdivision2: subdivision2 ? subdivision2 : null, city: city ? city : null, - url: url?.substring(0, URL_LENGTH), + urlPath: urlPath?.substring(0, URL_LENGTH), + urlQuery: urlQuery?.substring(0, URL_LENGTH), page_title: pageTitle, event_type: EVENT_TYPE.customEvent, event_name: eventName?.substring(0, EVENT_NAME_LENGTH), diff --git a/queries/analytics/pageview/getPageviewStats.ts b/queries/analytics/pageview/getPageviewStats.ts index b2d86b33..273151aa 100644 --- a/queries/analytics/pageview/getPageviewStats.ts +++ b/queries/analytics/pageview/getPageviewStats.ts @@ -50,7 +50,7 @@ async function relationalQuery( const { filterQuery, joinSession } = parseFilters(filters, params); return rawQuery( - `select ${getDateQuery('website_event.created_at', unit, timezone)} t, + `select ${getDateQuery('website_event.created_at', unit, timezone)} x, count(${count !== '*' ? `${count}${sessionKey}` : count}) y from website_event ${joinSession} @@ -83,7 +83,7 @@ async function clickhouseQuery( return rawQuery( `select - ${getDateStringQuery('g.t', unit)} as t, + ${getDateStringQuery('g.t', unit)} as x, g.y as y from (select diff --git a/queries/analytics/pageview/savePageView.ts b/queries/analytics/pageview/savePageView.ts index 845a73ad..2122f51d 100644 --- a/queries/analytics/pageview/savePageView.ts +++ b/queries/analytics/pageview/savePageView.ts @@ -8,8 +8,11 @@ import { uuid } from 'lib/crypto'; export async function savePageView(args: { id: string; websiteId: string; - url: string; - referrer?: string; + urlPath: string; + urlQuery?: string; + referrerPath?: string; + referrerQuery?: string; + referrerDomain?: string; pageTitle?: string; hostname?: string; browser?: string; @@ -31,19 +34,34 @@ export async function savePageView(args: { async function relationalQuery(data: { id: string; websiteId: string; - url: string; - referrer?: string; + urlPath: string; + urlQuery?: string; + referrerPath?: string; + referrerQuery?: string; + referrerDomain?: string; pageTitle?: string; }) { - const { websiteId, id: sessionId, url, referrer, pageTitle } = data; + const { + websiteId, + id: sessionId, + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, + pageTitle, + } = data; return prisma.client.websiteEvent.create({ data: { id: uuid(), websiteId, sessionId, - url: url?.substring(0, URL_LENGTH), - referrer: referrer?.substring(0, URL_LENGTH), + urlPath: urlPath?.substring(0, URL_LENGTH), + urlQuery: urlQuery?.substring(0, URL_LENGTH), + referrerPath: referrerPath?.substring(0, URL_LENGTH), + referrerQuery: referrerQuery?.substring(0, URL_LENGTH), + referrerDomain: referrerDomain?.substring(0, URL_LENGTH), pageTitle: pageTitle, eventType: EVENT_TYPE.pageView, }, @@ -53,8 +71,11 @@ async function relationalQuery(data: { async function clickhouseQuery(data: { id: string; websiteId: string; - url: string; - referrer?: string; + urlPath: string; + urlQuery?: string; + referrerPath?: string; + referrerQuery?: string; + referrerDomain?: string; pageTitle?: string; hostname?: string; browser?: string; @@ -70,8 +91,11 @@ async function clickhouseQuery(data: { const { websiteId, id: sessionId, - url, - referrer, + urlPath, + urlQuery, + referrerPath, + referrerQuery, + referrerDomain, pageTitle, country, subdivision1, @@ -91,8 +115,11 @@ async function clickhouseQuery(data: { subdivision1: subdivision1 ? subdivision1 : null, subdivision2: subdivision2 ? subdivision2 : null, city: city ? city : null, - url: url?.substring(0, URL_LENGTH), - referrer: referrer?.substring(0, URL_LENGTH), + urlPath: urlPath?.substring(0, URL_LENGTH), + urlQuery: urlQuery?.substring(0, URL_LENGTH), + referrerPath: referrerPath?.substring(0, URL_LENGTH), + referrerQuery: referrerQuery?.substring(0, URL_LENGTH), + referrerDomain: referrerDomain?.substring(0, URL_LENGTH), page_title: pageTitle, event_type: EVENT_TYPE.pageView, created_at: getDateFormat(new Date()), diff --git a/rollup.tracker.config.js b/rollup.tracker.config.js index 07b1e00c..f4e7223c 100644 --- a/rollup.tracker.config.js +++ b/rollup.tracker.config.js @@ -6,7 +6,7 @@ import { terser } from 'rollup-plugin-terser'; export default { input: 'tracker/index.js', output: { - file: 'public/umami.js', + file: 'public/script.js', format: 'iife', }, plugins: [ diff --git a/yarn.lock b/yarn.lock index 55d0b6e3..ede5b85e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1760,6 +1760,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + "@netlify/esbuild-android-64@0.14.39": version "0.14.39" resolved "https://registry.yarnpkg.com/@netlify/esbuild-android-64/-/esbuild-android-64-0.14.39.tgz#7bd30aba94a92351d2c5e25e178ceb824f3c2f99" @@ -3393,28 +3398,17 @@ chalk@^4.0.0, chalk@^4.1.1, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chart.js@^2.9.4: - version "2.9.4" - resolved "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz" - integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A== +chart.js@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.2.1.tgz#d2bd5c98e9a0ae35408975b638f40513b067ba1d" + integrity sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw== dependencies: - chartjs-color "^2.1.0" - moment "^2.10.2" + "@kurkle/color" "^0.3.0" -chartjs-color-string@^0.6.0: - version "0.6.0" - resolved "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz" - integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A== - dependencies: - color-name "^1.0.0" - -chartjs-color@^2.1.0: - version "2.4.1" - resolved "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz" - integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w== - dependencies: - chartjs-color-string "^0.6.0" - color-convert "^1.9.3" +chartjs-adapter-date-fns@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chartjs-adapter-date-fns/-/chartjs-adapter-date-fns-3.0.0.tgz#c25f63c7f317c1f96f9a7c44bd45eeedb8a478e5" + integrity sha512-Rs3iEB3Q5pJ973J93OBTpnP7qoGwvq3nUnoMdtxO+9aoJof7UFcRbWcIDteXuYd1fgAvct/32T9qaLyLuZVwCg== chokidar@^3.5.3: version "3.5.3" @@ -3509,7 +3503,7 @@ cluster-key-slot@^1.1.0: resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== -color-convert@^1.9.0, color-convert@^1.9.3: +color-convert@^1.9.0: version "1.9.3" resolved "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -6177,7 +6171,7 @@ moment-timezone@^0.5.35: dependencies: moment "^2.29.4" -"moment@>= 2.9.0", moment@^2.10.2, moment@^2.29.4: +"moment@>= 2.9.0", moment@^2.29.4: version "2.29.4" resolved "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz" integrity sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w== @@ -7142,10 +7136,10 @@ rc@^1.2.7: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-basics@^0.71.0: - version "0.71.0" - resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.71.0.tgz#317ab58cdbadd4ba36b233cc64dec2a64fe0f1b8" - integrity sha512-DLhx9bweJz2JG0lETnRrjQNeLL/pmyBqd0SFLM+VXaw8+6SnFheVhQ1Q/W8UerNRsN2oLGH4Hg1XuULG0JlrgA== +react-basics@^0.73.0: + version "0.73.0" + resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.73.0.tgz#9555563f3407ac417dc833dfca47588123d55535" + integrity sha512-eEK8yWWrXO7JATBlPKBfFQlD1hNZoNeEtlYNx+QjOCLKu1qjClutP5nXWHmX4gHE97XFwUKzbTU35NkNEy5C0w== dependencies: classnames "^2.3.1" date-fns "^2.29.3" @@ -7258,14 +7252,6 @@ react-spring@^9.5.5: "@react-spring/web" "~9.5.5" "@react-spring/zdog" "~9.5.5" -react-tooltip@^4.2.21: - version "4.5.1" - resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-4.5.1.tgz#77eccccdf16adec804132e558ec20ca5783b866a" - integrity sha512-Zo+CSFUGXar1uV+bgXFFDe7VeS2iByeIp5rTgTcc2HqtuOS5D76QapejNNfx320MCY91TlhTQat36KGFTqgcvw== - dependencies: - prop-types "^15.8.1" - uuid "^7.0.3" - react-use-measure@^2.0.4: version "2.1.1" resolved "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.1.tgz" @@ -8641,11 +8627,6 @@ uuid@3.4.0, uuid@^3.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b" - integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== - uuid@^8.3.0, uuid@^8.3.2: version "8.3.2" resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz"