Alleviate props passing: institute UserContext, QueryContext, SiteContext (#4334)

* Alleviate props passing: institute UserContext, QueryContext, SiteContext

* Remove unnecessary memo, fix SiteContext defaultValues
This commit is contained in:
Artur Pata 2024-07-10 17:02:05 +03:00 committed by GitHub
parent 645b81376c
commit 2dd2f058d1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 547 additions and 437 deletions

View File

@ -5,6 +5,7 @@
}, },
"extends": ["eslint:recommended", "extends": ["eslint:recommended",
"plugin:react/recommended", "plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier" "prettier"
], ],
"parser": "babel-eslint", "parser": "babel-eslint",

View File

@ -7,36 +7,16 @@ import ErrorBoundary from './dashboard/error-boundary'
import * as api from './dashboard/api' import * as api from './dashboard/api'
import * as timer from './dashboard/util/realtime-update-timer' import * as timer from './dashboard/util/realtime-update-timer'
import { filtersBackwardsCompatibilityRedirect } from './dashboard/query'; import { filtersBackwardsCompatibilityRedirect } from './dashboard/query';
import SiteContextProvider, { parseSiteFromDataset } from './dashboard/site-context';
import UserContextProvider from './dashboard/user-context'
timer.start() timer.start()
const container = document.getElementById('stats-react-container') const container = document.getElementById('stats-react-container')
if (container) { if (container) {
const site = { const site = parseSiteFromDataset(container.dataset)
domain: container.dataset.domain,
offset: container.dataset.offset,
hasGoals: container.dataset.hasGoals === 'true',
hasProps: container.dataset.hasProps === 'true',
funnelsAvailable: container.dataset.funnelsAvailable === 'true',
propsAvailable: container.dataset.propsAvailable === 'true',
conversionsOptedOut: container.dataset.conversionsOptedOut === 'true',
funnelsOptedOut: container.dataset.funnelsOptedOut === 'true',
propsOptedOut: container.dataset.propsOptedOut === 'true',
revenueGoals: JSON.parse(container.dataset.revenueGoals),
funnels: JSON.parse(container.dataset.funnels),
statsBegin: container.dataset.statsBegin,
nativeStatsBegin: container.dataset.nativeStatsBegin,
embedded: container.dataset.embedded,
background: container.dataset.background,
isDbip: container.dataset.isDbip === 'true',
flags: JSON.parse(container.dataset.flags),
validIntervalsByPeriod: JSON.parse(container.dataset.validIntervalsByPeriod),
shared: !!container.dataset.sharedLinkAuth,
}
const loggedIn = container.dataset.loggedIn === 'true'
const currentUserRole = container.dataset.currentUserRole
const sharedLinkAuth = container.dataset.sharedLinkAuth const sharedLinkAuth = container.dataset.sharedLinkAuth
if (sharedLinkAuth) { if (sharedLinkAuth) {
api.setSharedLinkAuth(sharedLinkAuth) api.setSharedLinkAuth(sharedLinkAuth)
@ -46,7 +26,11 @@ if (container) {
const app = ( const app = (
<ErrorBoundary> <ErrorBoundary>
<Router site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} /> <SiteContextProvider site={site}>
<UserContextProvider role={container.dataset.currentUserRole} loggedIn={container.dataset.loggedIn === 'true'}>
<Router />
</UserContextProvider>
</SiteContextProvider>
</ErrorBoundary> </ErrorBoundary>
) )

View File

@ -7,6 +7,8 @@ import classNames from 'classnames'
import * as storage from './util/storage' import * as storage from './util/storage'
import Flatpickr from 'react-flatpickr' import Flatpickr from 'react-flatpickr'
import { parseNaiveDate, formatISO, formatDateRange } from './util/date.js' import { parseNaiveDate, formatISO, formatDateRange } from './util/date.js'
import { useQueryContext } from './query-context.js'
import { useSiteContext } from './site-context.js'
const COMPARISON_MODES = { const COMPARISON_MODES = {
'off': 'Disable comparison', 'off': 'Disable comparison',
@ -19,11 +21,11 @@ const DEFAULT_COMPARISON_MODE = 'previous_period'
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all'] export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
export const getStoredMatchDayOfWeek = function(domain) { export const getStoredMatchDayOfWeek = function (domain) {
return storage.getItem(`comparison_match_day_of_week__${domain}`) || 'true' return storage.getItem(`comparison_match_day_of_week__${domain}`) || 'true'
} }
export const getStoredComparisonMode = function(domain) { export const getStoredComparisonMode = function (domain) {
const mode = storage.getItem(`comparison_mode__${domain}`) const mode = storage.getItem(`comparison_mode__${domain}`)
if (Object.keys(COMPARISON_MODES).includes(mode)) { if (Object.keys(COMPARISON_MODES).includes(mode)) {
return mode return mode
@ -32,16 +34,16 @@ export const getStoredComparisonMode = function(domain) {
} }
} }
const storeComparisonMode = function(domain, mode) { const storeComparisonMode = function (domain, mode) {
if (mode == "custom") return if (mode == "custom") return
storage.setItem(`comparison_mode__${domain}`, mode) storage.setItem(`comparison_mode__${domain}`, mode)
} }
export const isComparisonEnabled = function(mode) { export const isComparisonEnabled = function (mode) {
return mode && mode !== "off" return mode && mode !== "off"
} }
export const toggleComparisons = function(history, query, site) { export const toggleComparisons = function (history, query, site) {
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return
if (isComparisonEnabled(query.comparison)) { if (isComparisonEnabled(query.comparison)) {
@ -72,14 +74,14 @@ function ComparisonModeOption({ label, value, isCurrentlySelected, updateMode, s
"font-bold": isCurrentlySelected, "font-bold": isCurrentlySelected,
}) })
return <button className={buttonClass}>{ label }</button> return <button className={buttonClass}>{label}</button>
} }
const disabled = isCurrentlySelected && value !== "custom" const disabled = isCurrentlySelected && value !== "custom"
return ( return (
<Menu.Item key={value} onClick={click} disabled={disabled}> <Menu.Item key={value} onClick={click} disabled={disabled}>
{ render } {render}
</Menu.Item> </Menu.Item>
) )
} }
@ -112,7 +114,9 @@ function MatchDayOfWeekInput({ history, query, site }) {
</> </>
} }
const ComparisonInput = function({ site, query, history }) { const ComparisonInput = function ({ history }) {
const { query } = useQueryContext();
const site = useSiteContext();
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
if (!isComparisonEnabled(query.comparison)) return null if (!isComparisonEnabled(query.comparison)) return null
@ -129,9 +133,12 @@ const ComparisonInput = function({ site, query, history }) {
} }
} }
// eslint-disable-next-line react-hooks/rules-of-hooks
const calendar = React.useRef(null) const calendar = React.useRef(null)
// eslint-disable-next-line react-hooks/rules-of-hooks
const [uiMode, setUiMode] = React.useState("menu") const [uiMode, setUiMode] = React.useState("menu")
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => { React.useEffect(() => {
if (uiMode == "datepicker") { if (uiMode == "datepicker") {
setTimeout(() => calendar.current.flatpickr.open(), 100) setTimeout(() => calendar.current.flatpickr.open(), 100)
@ -162,7 +169,7 @@ const ComparisonInput = function({ site, query, history }) {
<div className="min-w-32 md:w-48 md:relative"> <div className="min-w-32 md:w-48 md:relative">
<Menu as="div" className="relative inline-block pl-2 w-full"> <Menu as="div" className="relative inline-block pl-2 w-full">
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow cursor-pointer w-full truncate"> <Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow cursor-pointer w-full truncate">
<span className="truncate">{ buildLabel(site, query) }</span> <span className="truncate">{buildLabel(site, query)}</span>
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-2" aria-hidden="true" /> <ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-2" aria-hidden="true" />
</Menu.Button> </Menu.Button>
<Transition <Transition
@ -174,18 +181,18 @@ const ComparisonInput = function({ site, query, history }) {
leaveFrom="opacity-100 scale-100" leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"> leaveTo="opacity-0 scale-95">
<Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static> <Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static>
{ Object.keys(COMPARISON_MODES).map((key) => ComparisonModeOption({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode, setUiMode })) } {Object.keys(COMPARISON_MODES).map((key) => ComparisonModeOption({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode, setUiMode }))}
{ query.comparison !== "custom" && <span> {query.comparison !== "custom" && <span>
<hr className="my-1" /> <hr className="my-1" />
<MatchDayOfWeekInput query={query} history={history} site={site} /> <MatchDayOfWeekInput query={query} history={history} site={site} />
</span>} </span>}
</Menu.Items> </Menu.Items>
</Transition> </Transition>
{ uiMode == "datepicker" && {uiMode == "datepicker" &&
<div className="h-0 md:absolute"> <div className="h-0 md:absolute">
<Flatpickr ref={calendar} options={flatpickrOptions} className="invisible" /> <Flatpickr ref={calendar} options={flatpickrOptions} className="invisible" />
</div> } </div>}
</Menu> </Menu>
</div> </div>
</div> </div>

View File

@ -1,50 +0,0 @@
import React, { useState, useEffect} from "react"
import * as api from '../api'
import { useMountedEffect } from '../custom-hooks'
import { parseQuery } from "../query"
// A Higher-Order component that tracks `query` state, and additional context
// related to it, such as:
// * `importedDataInView` - simple state with a `false` default. An
// `updateImportedDataInView` prop will be passed into the WrappedComponent
// and allows changing that according to responses from the API.
// * `lastLoadTimestamp` - used for displaying a tooltip with time passed since
// the last update in realtime components.
export default function withQueryContext(WrappedComponent) {
return (props) => {
const { site, location } = props
const [query, setQuery] = useState(parseQuery(location.search, site))
const [importedDataInView, setImportedDataInView] = useState(false)
const [lastLoadTimestamp, setLastLoadTimestamp] = useState(new Date())
const updateLastLoadTimestamp = () => { setLastLoadTimestamp(new Date()) }
useEffect(() => {
document.addEventListener('tick', updateLastLoadTimestamp)
return () => {
document.removeEventListener('tick', updateLastLoadTimestamp)
}
}, [])
useMountedEffect(() => {
api.cancelAll()
setQuery(parseQuery(location.search, site))
updateLastLoadTimestamp()
}, [location.search])
return (
<WrappedComponent
{...props}
query={query}
importedDataInView={importedDataInView}
updateImportedDataInView={setImportedDataInView}
lastLoadTimestamp={lastLoadTimestamp}
/>
)
}
}

View File

@ -28,6 +28,8 @@ import { navigateToQuery, QueryLink, QueryButton } from "./query";
import { shouldIgnoreKeypress } from "./keybinding.js" import { shouldIgnoreKeypress } from "./keybinding.js"
import { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js" import { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js"
import classNames from "classnames" import classNames from "classnames"
import { useQueryContext } from "./query-context.js";
import { useSiteContext } from "./site-context.js";
function renderArrow(query, site, period, prevDate, nextDate) { function renderArrow(query, site, period, prevDate, nextDate) {
const insertionDate = parseUTCDate(site.statsBegin); const insertionDate = parseUTCDate(site.statsBegin);
@ -156,7 +158,9 @@ function DisplayPeriod({ query, site }) {
return 'Realtime' return 'Realtime'
} }
function DatePicker({ query, site, history }) { function DatePicker({ history }) {
const { query } = useQueryContext();
const site = useSiteContext();
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const [mode, setMode] = useState('menu') const [mode, setMode] = useState('menu')
const dropDownNode = useRef(null) const dropDownNode = useRef(null)

View File

@ -16,6 +16,8 @@ import {
getLabel, getLabel,
FILTER_OPERATIONS_DISPLAY_NAMES FILTER_OPERATIONS_DISPLAY_NAMES
} from "./util/filters" } from "./util/filters"
import { useQueryContext } from './query-context';
import { useSiteContext } from './site-context';
const WRAPSTATE = { unwrapped: 0, waiting: 1, wrapped: 2 } const WRAPSTATE = { unwrapped: 0, waiting: 1, wrapped: 2 }
@ -122,8 +124,10 @@ function DropdownContent({ history, site, query, wrapped }) {
) )
} }
function Filters(props) { function Filters({ history }) {
const { history, query, site } = props const { query } = useQueryContext();
const site = useSiteContext();
const [wrapped, setWrapped] = useState(WRAPSTATE.waiting) const [wrapped, setWrapped] = useState(WRAPSTATE.waiting)
const [viewport, setViewport] = useState(1080) const [viewport, setViewport] = useState(1080)

View File

@ -13,45 +13,51 @@ import Behaviours from './stats/behaviours'
import ComparisonInput from './comparison-input' import ComparisonInput from './comparison-input'
import { withPinnedHeader } from './pinned-header-hoc'; import { withPinnedHeader } from './pinned-header-hoc';
import { statsBoxClass } from './index'; import { statsBoxClass } from './index';
import { useSiteContext } from './site-context';
import { useQueryContext } from './query-context';
import { useUserContext } from './user-context';
function Historical(props) { function Historical({ stuck, importedDataInView, updateImportedDataInView }) {
const site = useSiteContext();
const user = useUserContext();
const { query } = useQueryContext();
const tooltipBoundary = React.useRef(null) const tooltipBoundary = React.useRef(null)
return ( return (
<div className="mb-12"> <div className="mb-12">
<div id="stats-container-top"></div> <div id="stats-container-top"></div>
<div className={`relative top-0 sm:py-3 py-2 z-10 ${props.stuck && !props.site.embedded ? 'sticky fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}> <div className={`relative top-0 sm:py-3 py-2 z-10 ${stuck && !site.embedded ? 'sticky fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}>
<div className="items-center w-full flex"> <div className="items-center w-full flex">
<div className="flex items-center w-full" ref={tooltipBoundary}> <div className="flex items-center w-full" ref={tooltipBoundary}>
<SiteSwitcher site={props.site} loggedIn={props.loggedIn} currentUserRole={props.currentUserRole} /> <SiteSwitcher site={site} loggedIn={user.loggedIn} currentUserRole={user.role} />
<CurrentVisitors site={props.site} query={props.query} lastLoadTimestamp={props.lastLoadTimestamp} tooltipBoundary={tooltipBoundary.current} /> <CurrentVisitors tooltipBoundary={tooltipBoundary.current} />
<Filters className="flex" site={props.site} query={props.query} history={props.history} /> <Filters className="flex" />
</div> </div>
<Datepicker site={props.site} query={props.query} /> <Datepicker />
<ComparisonInput site={props.site} query={props.query} /> <ComparisonInput />
</div> </div>
</div> </div>
<VisitorGraph site={props.site} query={props.query} updateImportedDataInView={props.updateImportedDataInView}/> <VisitorGraph updateImportedDataInView={updateImportedDataInView} />
<div className="w-full md:flex"> <div className="w-full md:flex">
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Sources site={props.site} query={props.query} /> <Sources />
</div> </div>
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Pages site={props.site} query={props.query} /> <Pages />
</div> </div>
</div> </div>
<div className="w-full md:flex"> <div className="w-full md:flex">
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Locations site={props.site} query={props.query} /> <Locations site={site} query={query} />
</div> </div>
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Devices site={props.site} query={props.query} /> <Devices />
</div> </div>
</div> </div>
<Behaviours site={props.site} query={props.query} currentUserRole={props.currentUserRole} importedDataInView={props.importedDataInView}/> <Behaviours importedDataInView={importedDataInView} />
</div> </div>
) )
} }

View File

@ -1,46 +1,26 @@
import React from 'react' import React, { useState } from 'react';
import { withRouter } from 'react-router-dom'
import Historical from './historical' import Historical from './historical'
import Realtime from './realtime' import Realtime, { useIsRealtimeDashboard } from './realtime'
import withQueryContext from './components/query-context-hoc';
export const statsBoxClass = "stats-item relative w-full mt-6 p-4 flex flex-col bg-white dark:bg-gray-825 shadow-xl rounded" export const statsBoxClass = "stats-item relative w-full mt-6 p-4 flex flex-col bg-white dark:bg-gray-825 shadow-xl rounded"
function Dashboard(props) { export function Dashboard() {
const { const isRealTimeDashboard = useIsRealtimeDashboard();
site, const [importedDataInView, setImportedDataInView] = useState(false)
loggedIn,
currentUserRole,
query,
importedDataInView,
updateImportedDataInView,
lastLoadTimestamp
} = props
if (query.period === 'realtime') { if (isRealTimeDashboard) {
return ( return (
<Realtime <Realtime />
site={site}
loggedIn={loggedIn}
currentUserRole={currentUserRole}
query={query}
lastLoadTimestamp={lastLoadTimestamp}
/>
) )
} else { } else {
return ( return (
<Historical <Historical
site={site}
loggedIn={loggedIn}
currentUserRole={currentUserRole}
query={query}
lastLoadTimestamp={lastLoadTimestamp}
importedDataInView={importedDataInView} importedDataInView={importedDataInView}
updateImportedDataInView={updateImportedDataInView} updateImportedDataInView={setImportedDataInView}
/> />
) )
} }
} }
export default withRouter(withQueryContext(Dashboard)) export default Dashboard

View File

@ -0,0 +1,40 @@
import React, { createContext, useMemo, useEffect, useContext, useState, useCallback } from "react";
import { parseQuery } from "./query";
import { withRouter } from "react-router-dom";
import { useMountedEffect } from "./custom-hooks";
import * as api from './api'
import { useSiteContext } from "./site-context";
const queryContextDefaultValue = { query: {}, lastLoadTimestamp: new Date() }
const QueryContext = createContext(queryContextDefaultValue)
export const useQueryContext = () => { return useContext(QueryContext) }
function QueryContextProvider({ location, children }) {
const site = useSiteContext();
const { search } = location;
const query = useMemo(() => {
return parseQuery(search, site)
}, [search, site])
const [lastLoadTimestamp, setLastLoadTimestamp] = useState(new Date())
const updateLastLoadTimestamp = useCallback(() => { setLastLoadTimestamp(new Date()) }, [setLastLoadTimestamp])
useEffect(() => {
document.addEventListener('tick', updateLastLoadTimestamp)
return () => {
document.removeEventListener('tick', updateLastLoadTimestamp)
}
}, [updateLastLoadTimestamp])
useMountedEffect(() => {
api.cancelAll()
updateLastLoadTimestamp()
}, [])
return <QueryContext.Provider value={{ query, lastLoadTimestamp }}>{children}</QueryContext.Provider>
};
export default withRouter(QueryContextProvider);

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useMemo } from 'react';
import Datepicker from './datepicker' import Datepicker from './datepicker'
import SiteSwitcher from './site-switcher' import SiteSwitcher from './site-switcher'
@ -11,9 +11,20 @@ import Devices from './stats/devices'
import Behaviours from './stats/behaviours' import Behaviours from './stats/behaviours'
import { withPinnedHeader } from './pinned-header-hoc'; import { withPinnedHeader } from './pinned-header-hoc';
import { statsBoxClass } from './index'; import { statsBoxClass } from './index';
import { useSiteContext } from './site-context';
import { useUserContext } from './user-context';
import { useQueryContext } from './query-context';
import { isRealTimeDashboard } from './util/filters';
function Realtime(props) { export const useIsRealtimeDashboard = () => {
const {site, query, history, stuck, loggedIn, currentUserRole, lastLoadTimestamp} = props const { query: { period } } = useQueryContext();
return useMemo(() => isRealTimeDashboard({ period }), [period]);
}
function Realtime({ stuck }) {
const site = useSiteContext();
const user = useUserContext();
const { query } = useQueryContext();
const navClass = site.embedded ? 'relative' : 'sticky' const navClass = site.embedded ? 'relative' : 'sticky'
return ( return (
@ -22,30 +33,30 @@ function Realtime(props) {
<div className={`${navClass} top-0 sm:py-3 py-2 z-10 ${stuck && !site.embedded ? 'fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}> <div className={`${navClass} top-0 sm:py-3 py-2 z-10 ${stuck && !site.embedded ? 'fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}>
<div className="items-center w-full flex"> <div className="items-center w-full flex">
<div className="flex items-center w-full"> <div className="flex items-center w-full">
<SiteSwitcher site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} /> <SiteSwitcher site={site} loggedIn={user.loggedIn} currentUserRole={user.role} />
<Filters className="flex" site={site} query={query} history={history} /> <Filters className="flex" />
</div> </div>
<Datepicker site={site} query={query} /> <Datepicker />
</div> </div>
</div> </div>
<VisitorGraph site={site} query={query} lastLoadTimestamp={lastLoadTimestamp}/> <VisitorGraph />
<div className="w-full md:flex"> <div className="w-full md:flex">
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Sources site={site} query={query} /> <Sources />
</div> </div>
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Pages site={site} query={query} /> <Pages />
</div> </div>
</div> </div>
<div className="w-full md:flex"> <div className="w-full md:flex">
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Locations site={site} query={query} /> <Locations site={site} query={query} />
</div> </div>
<div className={ statsBoxClass }> <div className={statsBoxClass}>
<Devices site={site} query={query} /> <Devices />
</div> </div>
</div> </div>
<Behaviours site={site} query={query} currentUserRole={currentUserRole} /> <Behaviours />
</div> </div>
) )
} }

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { BrowserRouter, Switch, Route, useLocation } from "react-router-dom"; import { BrowserRouter, Switch, Route, useLocation } from "react-router-dom";
import Dash from './index' import Dashboard from './index'
import SourcesModal from './stats/modals/sources' import SourcesModal from './stats/modals/sources'
import ReferrersDrilldownModal from './stats/modals/referrer-drilldown' import ReferrersDrilldownModal from './stats/modals/referrer-drilldown'
import GoogleKeywordsModal from './stats/modals/google-keywords' import GoogleKeywordsModal from './stats/modals/google-keywords'
@ -12,6 +12,8 @@ import LocationsModal from './stats/modals/locations-modal';
import PropsModal from './stats/modals/props' import PropsModal from './stats/modals/props'
import ConversionsModal from './stats/modals/conversions' import ConversionsModal from './stats/modals/conversions'
import FilterModal from './stats/modals/filter-modal' import FilterModal from './stats/modals/filter-modal'
import QueryContextProvider from './query-context';
import { useSiteContext } from './site-context';
function ScrollToTop() { function ScrollToTop() {
const location = useLocation(); const location = useLocation();
@ -25,45 +27,48 @@ function ScrollToTop() {
return null; return null;
} }
export default function Router({ site, loggedIn, currentUserRole }) { export default function Router() {
const site = useSiteContext()
return ( return (
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}> <BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
<Route path="/"> <QueryContextProvider>
<ScrollToTop /> <Route path="/">
<Dash site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} /> <ScrollToTop />
<Switch> <Dashboard />
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}> <Switch>
<SourcesModal site={site} /> <Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
</Route> <SourcesModal />
<Route exact path="/referrers/Google"> </Route>
<GoogleKeywordsModal site={site} /> <Route exact path="/referrers/Google">
</Route> <GoogleKeywordsModal site={site} />
<Route exact path="/referrers/:referrer"> </Route>
<ReferrersDrilldownModal site={site} /> <Route exact path="/referrers/:referrer">
</Route> <ReferrersDrilldownModal />
<Route path="/pages"> </Route>
<PagesModal site={site} /> <Route path="/pages">
</Route> <PagesModal />
<Route path="/entry-pages"> </Route>
<EntryPagesModal site={site} /> <Route path="/entry-pages">
</Route> <EntryPagesModal />
<Route path="/exit-pages"> </Route>
<ExitPagesModal site={site} /> <Route path="/exit-pages">
</Route> <ExitPagesModal />
<Route exact path={["/countries", "/regions", "/cities"]}> </Route>
<LocationsModal site={site} /> <Route exact path={["/countries", "/regions", "/cities"]}>
</Route> <LocationsModal />
<Route path="/custom-prop-values/:prop_key"> </Route>
<PropsModal site={site} /> <Route path="/custom-prop-values/:prop_key">
</Route> <PropsModal />
<Route path="/conversions"> </Route>
<ConversionsModal site={site} /> <Route path="/conversions">
</Route> <ConversionsModal />
<Route path={["/filter/:field"]}> </Route>
<FilterModal site={site} /> <Route path={["/filter/:field"]}>
</Route> <FilterModal site={site} />
</Switch> </Route>
</Route> </Switch>
</BrowserRouter > </Route>
</QueryContextProvider>
</BrowserRouter>
); );
} }

View File

@ -0,0 +1,59 @@
import React, { createContext, useContext } from "react";
export function parseSiteFromDataset(dataset) {
const site = {
domain: dataset.domain,
offset: dataset.offset,
hasGoals: dataset.hasGoals === 'true',
hasProps: dataset.hasProps === 'true',
funnelsAvailable: dataset.funnelsAvailable === 'true',
propsAvailable: dataset.propsAvailable === 'true',
conversionsOptedOut: dataset.conversionsOptedOut === 'true',
funnelsOptedOut: dataset.funnelsOptedOut === 'true',
propsOptedOut: dataset.propsOptedOut === 'true',
revenueGoals: JSON.parse(dataset.revenueGoals),
funnels: JSON.parse(dataset.funnels),
statsBegin: dataset.statsBegin,
nativeStatsBegin: dataset.nativeStatsBegin,
embedded: dataset.embedded,
background: dataset.background,
isDbip: dataset.isDbip === 'true',
flags: JSON.parse(dataset.flags),
validIntervalsByPeriod: JSON.parse(dataset.validIntervalsByPeriod),
shared: !!dataset.sharedLinkAuth,
}
return site;
}
const siteContextDefaultValue = {
domain: '',
offset: '0',
hasGoals: false,
hasProps: false,
funnelsAvailable: false,
propsAvailable: false,
conversionsOptedOut: false,
funnelsOptedOut: false,
propsOptedOut: false,
revenueGoals: [],
funnels: [],
statsBegin: '',
nativeStatsBegin: '',
embedded: '',
background: '',
isDbip: false,
flags: {},
validIntervalsByPeriod: null,
shared: false,
}
const SiteContext = createContext(siteContextDefaultValue)
export const useSiteContext = () => { return useContext(SiteContext) }
const SiteContextProvider = ({ site, children }) => {
return <SiteContext.Provider value={site}>{children}</SiteContext.Provider>
};
export default SiteContextProvider;

View File

@ -9,6 +9,9 @@ import Properties from './props'
import { FeatureSetupNotice } from '../../components/notice' import { FeatureSetupNotice } from '../../components/notice'
import { SPECIAL_GOALS } from './goal-conversions' import { SPECIAL_GOALS } from './goal-conversions'
import { hasGoalFilter } from '../../util/filters' import { hasGoalFilter } from '../../util/filters'
import { useSiteContext } from '../../site-context'
import { useQueryContext } from '../../query-context'
import { useUserContext } from '../../user-context'
/*global BUILD_EXTRA*/ /*global BUILD_EXTRA*/
/*global require*/ /*global require*/
@ -35,9 +38,12 @@ export const sectionTitles = {
[FUNNELS]: 'Funnels' [FUNNELS]: 'Funnels'
} }
export default function Behaviours(props) { export default function Behaviours({ importedDataInView }) {
const { site, query, currentUserRole } = props const { query } = useQueryContext();
const adminAccess = ['owner', 'admin', 'super_admin'].includes(currentUserRole) const site = useSiteContext();
const user = useUserContext();
const adminAccess = ['owner', 'admin', 'super_admin'].includes(user.role)
const tabKey = `behavioursTab__${site.domain}` const tabKey = `behavioursTab__${site.domain}`
const funnelKey = `behavioursTabFunnel__${site.domain}` const funnelKey = `behavioursTabFunnel__${site.domain}`
const [enabledModes, setEnabledModes] = useState(getEnabledModes()) const [enabledModes, setEnabledModes] = useState(getEnabledModes())
@ -309,7 +315,7 @@ export default function Behaviours(props) {
// If the feature is not supported by the site owner's subscription, // If the feature is not supported by the site owner's subscription,
// it only makes sense to display the feature tab to the owner itself // it only makes sense to display the feature tab to the owner itself
// as only they can upgrade to make the feature available. // as only they can upgrade to make the feature available.
const callToActionIsMissing = !isAvailable && currentUserRole !== 'owner' const callToActionIsMissing = !isAvailable && user.role !== 'owner'
if (!isOptedOut && !callToActionIsMissing) { if (!isOptedOut && !callToActionIsMissing) {
enabledModes.push(feature) enabledModes.push(feature)
@ -341,7 +347,7 @@ export default function Behaviours(props) {
} else if (mode === PROPS) { } else if (mode === PROPS) {
return <ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason} message="Imported data is unavailable in this view" /> return <ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason} message="Imported data is unavailable in this view" />
} else { } else {
return <ImportedQueryUnsupportedWarning altCondition={props.importedDataInView} message="Imported data is unavailable in this view" /> return <ImportedQueryUnsupportedWarning altCondition={importedDataInView} message="Imported data is unavailable in this view" />
} }
} }

View File

@ -4,9 +4,12 @@ import * as api from '../api'
import * as url from '../util/url' import * as url from '../util/url'
import { Tooltip } from '../util/tooltip'; import { Tooltip } from '../util/tooltip';
import { SecondsSinceLastLoad } from '../util/seconds-since-last-load'; import { SecondsSinceLastLoad } from '../util/seconds-since-last-load';
import { useQueryContext } from '../query-context';
import { useSiteContext } from '../site-context';
export default function CurrentVisitors(props) { export default function CurrentVisitors({ tooltipBoundary }) {
const { site, query, lastLoadTimestamp, tooltipBoundary } = props const { query, lastLoadTimestamp } = useQueryContext();
const site = useSiteContext();
const [currentVisitors, setCurrentVisitors] = useState(null) const [currentVisitors, setCurrentVisitors] = useState(null)
const updateCount = useCallback(() => { const updateCount = useCallback(() => {

View File

@ -1,4 +1,4 @@
import React, {useEffect, useState} from 'react'; import React, { useEffect, useState } from 'react';
import * as storage from '../../util/storage' import * as storage from '../../util/storage'
import { getFiltersByKeyPrefix, hasGoalFilter, isFilteringOnFixedValue } from '../../util/filters' import { getFiltersByKeyPrefix, hasGoalFilter, isFilteringOnFixedValue } from '../../util/filters'
import ListReport from '../reports/list' import ListReport from '../reports/list'
@ -6,6 +6,8 @@ import * as metrics from '../reports/metrics'
import * as api from '../../api' import * as api from '../../api'
import * as url from '../../util/url' import * as url from '../../util/url'
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning'; import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
import { useQueryContext } from '../../query-context';
import { useSiteContext } from '../../site-context';
// Icons copied from https://github.com/alrra/browser-logos // Icons copied from https://github.com/alrra/browser-logos
const BROWSER_ICONS = { const BROWSER_ICONS = {
@ -57,7 +59,7 @@ function Browsers({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({ meta: {plot: true}}), metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage() !hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric) ].filter(metric => !!metric)
@ -80,9 +82,11 @@ function BrowserVersions({ query, site, afterFetchData }) {
function fetchData() { function fetchData() {
return api.get(url.apiPath(site, '/browser-versions'), query) return api.get(url.apiPath(site, '/browser-versions'), query)
.then(res => { .then(res => {
return {...res, results: res.results.map((row => { return {
return {...row, name: `${row.browser} ${row.name}`, version: row.name} ...res, results: res.results.map((row => {
}))} return { ...row, name: `${row.browser} ${row.name}`, version: row.name }
}))
}
}) })
} }
@ -102,7 +106,7 @@ function BrowserVersions({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({ meta: {plot: true}}), metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage() !hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric) ].filter(metric => !!metric)
@ -166,9 +170,9 @@ function OperatingSystems({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({ meta: {plot: true}}), metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage({meta: {hiddenonMobile: true}}) !hasGoalFilter(query) && metrics.createPercentage({ meta: { hiddenonMobile: true } })
].filter(metric => !!metric) ].filter(metric => !!metric)
} }
@ -193,9 +197,11 @@ function OperatingSystemVersions({ query, site, afterFetchData }) {
function fetchData() { function fetchData() {
return api.get(url.apiPath(site, '/operating-system-versions'), query) return api.get(url.apiPath(site, '/operating-system-versions'), query)
.then(res => { .then(res => {
return {...res, results: res.results.map((row => { return {
return {...row, name: `${row.os} ${row.name}`, version: row.name} ...res, results: res.results.map((row => {
}))} return { ...row, name: `${row.os} ${row.name}`, version: row.name }
}))
}
}) })
} }
@ -215,7 +221,7 @@ function OperatingSystemVersions({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({ meta: {plot: true}}), metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage() !hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric) ].filter(metric => !!metric)
@ -255,7 +261,7 @@ function ScreenSizes({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({ meta: {plot: true}}), metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage() !hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric) ].filter(metric => !!metric)
@ -296,8 +302,10 @@ function iconFor(screenSize) {
} }
} }
export default function Devices(props) { export default function Devices() {
const {site, query} = props const { query } = useQueryContext();
const site = useSiteContext();
const tabKey = `deviceTab__${site.domain}` const tabKey = `deviceTab__${site.domain}`
const storedTab = storage.getItem(tabKey) const storedTab = storage.getItem(tabKey)
const [mode, setMode] = useState(storedTab || 'browser') const [mode, setMode] = useState(storedTab || 'browser')
@ -362,7 +370,7 @@ export default function Devices(props) {
<div className="flex justify-between w-full"> <div className="flex justify-between w-full">
<div className="flex gap-x-1"> <div className="flex gap-x-1">
<h3 className="font-bold dark:text-gray-100">Devices</h3> <h3 className="font-bold dark:text-gray-100">Devices</h3>
<ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason}/> <ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason} />
</div> </div>
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2"> <div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
{renderPill('Browser', 'browser')} {renderPill('Browser', 'browser')}

View File

@ -64,10 +64,12 @@ function storeInterval(period, domain, interval) {
} }
function subscribeKeybinding(element) { function subscribeKeybinding(element) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleKeyPress = useCallback((event) => { const handleKeyPress = useCallback((event) => {
if (isKeyPressed(event, "i")) element.current?.click() if (isKeyPressed(event, "i")) element.current?.click()
}, []) }, [])
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => { useEffect(() => {
document.addEventListener('keydown', handleKeyPress) document.addEventListener('keydown', handleKeyPress)
return () => document.removeEventListener('keydown', handleKeyPress) return () => document.removeEventListener('keydown', handleKeyPress)
@ -90,6 +92,7 @@ export const getCurrentInterval = function(site, query) {
export function IntervalPicker({ query, site, onIntervalUpdate }) { export function IntervalPicker({ query, site, onIntervalUpdate }) {
if (query.period == 'realtime') return null if (query.period == 'realtime') return null
// eslint-disable-next-line react-hooks/rules-of-hooks
const menuElement = React.useRef(null) const menuElement = React.useRef(null)
const options = validIntervals(site, query) const options = validIntervals(site, query)
const currentInterval = getCurrentInterval(site, query) const currentInterval = getCurrentInterval(site, query)

View File

@ -11,6 +11,8 @@ import FadeIn from '../../fade-in';
import * as url from '../../util/url' import * as url from '../../util/url'
import { isComparisonEnabled } from '../../comparison-input' import { isComparisonEnabled } from '../../comparison-input'
import LineGraphWithRouter from './line-graph' import LineGraphWithRouter from './line-graph'
import { useQueryContext } from '../../query-context';
import { useSiteContext } from '../../site-context';
function fetchTopStats(site, query) { function fetchTopStats(site, query) {
const q = { ...query } const q = { ...query }
@ -23,12 +25,14 @@ function fetchTopStats(site, query) {
} }
function fetchMainGraph(site, query, metric, interval) { function fetchMainGraph(site, query, metric, interval) {
const params = {metric, interval} const params = { metric, interval }
return api.get(url.apiPath(site, '/main-graph'), query, params) return api.get(url.apiPath(site, '/main-graph'), query, params)
} }
export default function VisitorGraph(props) { export default function VisitorGraph({ updateImportedDataInView }) {
const {site, query, lastLoadTimestamp} = props const { query, lastLoadTimestamp } = useQueryContext();
const site = useSiteContext();
const isRealtime = query.period === 'realtime' const isRealtime = query.period === 'realtime'
const isDarkTheme = document.querySelector('html').classList.contains('dark') || false const isDarkTheme = document.querySelector('html').classList.contains('dark') || false
@ -81,8 +85,8 @@ export default function VisitorGraph(props) {
function fetchTopStatsAndGraphData() { function fetchTopStatsAndGraphData() {
fetchTopStats(site, query) fetchTopStats(site, query)
.then((res) => { .then((res) => {
if (props.updateImportedDataInView) { if (updateImportedDataInView) {
props.updateImportedDataInView(res.includes_imported) updateImportedDataInView(res.includes_imported)
} }
setTopStatData(res) setTopStatData(res)
setTopStatsLoading(false) setTopStatsLoading(false)
@ -149,7 +153,7 @@ export default function VisitorGraph(props) {
{graphRefreshing && renderLoader()} {graphRefreshing && renderLoader()}
<div className="absolute right-4 -top-8 py-1 flex items-center"> <div className="absolute right-4 -top-8 py-1 flex items-center">
{!isRealtime && <StatsExport site={site} query={query} />} {!isRealtime && <StatsExport site={site} query={query} />}
<SamplingNotice samplePercent={topStatData}/> <SamplingNotice samplePercent={topStatData} />
<WithImportedSwitch query={query} info={topStatData && topStatData.with_imported_switch} /> <WithImportedSwitch query={query} info={topStatData && topStatData.with_imported_switch} />
<IntervalPicker site={site} query={query} onIntervalUpdate={onIntervalUpdate} /> <IntervalPicker site={site} query={query} onIntervalUpdate={onIntervalUpdate} />
</div> </div>

View File

@ -4,6 +4,8 @@ import * as api from '../../api'
import { useDebouncedEffect, useMountedEffect } from '../../custom-hooks' import { useDebouncedEffect, useMountedEffect } from '../../custom-hooks'
import { trimURL } from '../../util/url' import { trimURL } from '../../util/url'
import { FilterLink } from "../reports/list"; import { FilterLink } from "../reports/list";
import { useQueryContext } from "../../query-context";
import { useSiteContext } from "../../site-context";
const LIMIT = 100 const LIMIT = 100
const MIN_HEIGHT_PX = 500 const MIN_HEIGHT_PX = 500
@ -33,11 +35,6 @@ const MIN_HEIGHT_PX = 500
// ### Required Props // ### Required Props
// * `site` - the current dashboard site
// * `query` - a read-only query object representing the query state of the
// dashboard (e.g. `filters`, `period`, `with_imported`, etc)
// * `reportInfo` - a map with the following required keys: // * `reportInfo` - a map with the following required keys:
// * `title` - the title of the report to render on the top left // * `title` - the title of the report to render on the top left
@ -77,8 +74,6 @@ const MIN_HEIGHT_PX = 500
// * `afterFetchNextPage` - a function with the same behaviour as `afterFetchData`, // * `afterFetchNextPage` - a function with the same behaviour as `afterFetchData`,
// but will be called after a successful next page load in `fetchNextPage`. // but will be called after a successful next page load in `fetchNextPage`.
export default function BreakdownModal({ export default function BreakdownModal({
site,
query,
reportInfo, reportInfo,
metrics, metrics,
renderIcon, renderIcon,
@ -89,6 +84,9 @@ export default function BreakdownModal({
addSearchFilter, addSearchFilter,
getFilterInfo getFilterInfo
}) { }) {
const {query} = useQueryContext();
const site = useSiteContext();
const endpoint = `/api/stats/${encodeURIComponent(site.domain)}${reportInfo.endpoint}` const endpoint = `/api/stats/${encodeURIComponent(site.domain)}${reportInfo.endpoint}`
const [initialLoading, setInitialLoading] = useState(true) const [initialLoading, setInitialLoading] = useState(true)

View File

@ -1,15 +1,11 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics"; import * as metrics from "../reports/metrics";
/*global BUILD_EXTRA*/ /*global BUILD_EXTRA*/
function ConversionsModal(props) { function ConversionsModal() {
const { site, query } = props
const [showRevenue, setShowRevenue] = useState(false) const [showRevenue, setShowRevenue] = useState(false)
const reportInfo = { const reportInfo = {
@ -28,8 +24,8 @@ function ConversionsModal(props) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({renderLabel: (_query) => "Uniques"}), metrics.createVisitors({ renderLabel: (_query) => "Uniques" }),
metrics.createEvents({renderLabel: (_query) => "Total"}), metrics.createEvents({ renderLabel: (_query) => "Total" }),
metrics.createConversionRate(), metrics.createConversionRate(),
showRevenue && metrics.createAverageRevenue(), showRevenue && metrics.createAverageRevenue(),
showRevenue && metrics.createTotalRevenue(), showRevenue && metrics.createTotalRevenue(),
@ -54,10 +50,8 @@ function ConversionsModal(props) {
} }
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
afterFetchData={BUILD_EXTRA ? afterFetchData : undefined} afterFetchData={BUILD_EXTRA ? afterFetchData : undefined}
@ -69,4 +63,4 @@ function ConversionsModal(props) {
) )
} }
export default withRouter(withQueryContext(ConversionsModal)) export default ConversionsModal

View File

@ -1,14 +1,13 @@
import React, {useCallback} from "react"; import React, { useCallback } from "react";
import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import { hasGoalFilter } from "../../util/filters"; import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import { addFilter } from '../../query' import { addFilter } from '../../query'
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from '../reports/metrics' import * as metrics from '../reports/metrics'
import withQueryContext from "../../components/query-context-hoc"; import { useQueryContext } from "../../query-context";
function EntryPagesModal(props) { function EntryPagesModal() {
const { site, query } = props const { query } = useQueryContext();
const reportInfo = { const reportInfo = {
title: 'Entry Pages', title: 'Entry Pages',
@ -32,29 +31,27 @@ function EntryPagesModal(props) {
if (hasGoalFilter(query)) { if (hasGoalFilter(query)) {
return [ return [
metrics.createTotalVisitors(), metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}), metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate() metrics.createConversionRate()
] ]
} }
if (query.period === 'realtime') { if (isRealTimeDashboard(query)) {
return [ return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'}) metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
] ]
} }
return [ return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }), metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createVisits({renderLabel: (_query) => "Total Entrances" }), metrics.createVisits({ renderLabel: (_query) => "Total Entrances" }),
metrics.createVisitDuration() metrics.createVisitDuration()
] ]
} }
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
getFilterInfo={getFilterInfo} getFilterInfo={getFilterInfo}
@ -64,4 +61,4 @@ function EntryPagesModal(props) {
) )
} }
export default withRouter(withQueryContext(EntryPagesModal)) export default EntryPagesModal

View File

@ -1,14 +1,13 @@
import React, {useCallback} from "react"; import React, { useCallback } from "react";
import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import { hasGoalFilter } from "../../util/filters"; import { hasGoalFilter } from "../../util/filters";
import { addFilter } from '../../query' import { addFilter } from '../../query'
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from '../reports/metrics' import * as metrics from '../reports/metrics'
import withQueryContext from "../../components/query-context-hoc"; import { useQueryContext } from "../../query-context";
function ExitPagesModal(props) { function ExitPagesModal() {
const { site, query } = props const { query } = useQueryContext();
const reportInfo = { const reportInfo = {
title: 'Exit Pages', title: 'Exit Pages',
@ -32,29 +31,27 @@ function ExitPagesModal(props) {
if (hasGoalFilter(query)) { if (hasGoalFilter(query)) {
return [ return [
metrics.createTotalVisitors(), metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}), metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate() metrics.createConversionRate()
] ]
} }
if (query.period === 'realtime') { if (query.period === 'realtime') {
return [ return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'}) metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
] ]
} }
return [ return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }), metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createVisits({renderLabel: (_query) => "Total Exits" }), metrics.createVisits({ renderLabel: (_query) => "Total Exits" }),
metrics.createExitRate() metrics.createExitRate()
] ]
} }
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
getFilterInfo={getFilterInfo} getFilterInfo={getFilterInfo}
@ -64,4 +61,4 @@ function ExitPagesModal(props) {
) )
} }
export default withRouter(withQueryContext(ExitPagesModal)) export default ExitPagesModal

View File

@ -2,19 +2,19 @@ import React, { useCallback } from "react";
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import { hasGoalFilter } from "../../util/filters"; import { hasGoalFilter } from "../../util/filters";
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics"; import * as metrics from "../reports/metrics";
import { useQueryContext } from "../../query-context";
const VIEWS = { const VIEWS = {
countries: {title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country'}, countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country' },
regions: {title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region'}, regions: { title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region' },
cities: {title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City'}, cities: { title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City' },
} }
function LocationsModal(props) { function LocationsModal({ location }) {
const { site, query, location } = props const { query } = useQueryContext();
const urlParts = location.pathname.split('/') const urlParts = location.pathname.split('/')
const currentView = urlParts[urlParts.length - 1] const currentView = urlParts[urlParts.length - 1]
@ -32,19 +32,19 @@ function LocationsModal(props) {
if (hasGoalFilter(query)) { if (hasGoalFilter(query)) {
return [ return [
metrics.createTotalVisitors(), metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}), metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate() metrics.createConversionRate()
] ]
} }
if (query.period === 'realtime') { if (query.period === 'realtime') {
return [ return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'}) metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
] ]
} }
return [ return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }), metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
currentView === 'countries' && metrics.createPercentage() currentView === 'countries' && metrics.createPercentage()
].filter(metric => !!metric) ].filter(metric => !!metric)
} }
@ -56,10 +56,8 @@ function LocationsModal(props) {
}, []) }, [])
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
getFilterInfo={getFilterInfo} getFilterInfo={getFilterInfo}
@ -70,4 +68,4 @@ function LocationsModal(props) {
) )
} }
export default withRouter(withQueryContext(LocationsModal)) export default withRouter(LocationsModal)

View File

@ -1,14 +1,13 @@
import React, {useCallback} from "react"; import React, {useCallback} from "react";
import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import { hasGoalFilter } from "../../util/filters"; import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import { addFilter } from '../../query' import { addFilter } from '../../query'
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from '../reports/metrics' import * as metrics from '../reports/metrics'
import withQueryContext from "../../components/query-context-hoc"; import { useQueryContext } from "../../query-context";
function PagesModal(props) { function PagesModal() {
const { site, query } = props const { query } = useQueryContext();
const reportInfo = { const reportInfo = {
title: 'Top Pages', title: 'Top Pages',
@ -37,7 +36,7 @@ function PagesModal(props) {
] ]
} }
if (query.period === 'realtime') { if (isRealTimeDashboard(query)) {
return [ return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'}) metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
] ]
@ -52,10 +51,8 @@ function PagesModal(props) {
} }
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
getFilterInfo={getFilterInfo} getFilterInfo={getFilterInfo}
@ -65,4 +62,4 @@ function PagesModal(props) {
) )
} }
export default withRouter(withQueryContext(PagesModal)) export default PagesModal

View File

@ -2,16 +2,18 @@ import React, { useCallback } from "react";
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import { addFilter } from '../../query' import { addFilter } from '../../query'
import { specialTitleWhenGoalFilter } from "../behaviours/goal-conversions"; import { specialTitleWhenGoalFilter } from "../behaviours/goal-conversions";
import { EVENT_PROPS_PREFIX, hasGoalFilter } from "../../util/filters" import { EVENT_PROPS_PREFIX, hasGoalFilter } from "../../util/filters"
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics"; import * as metrics from "../reports/metrics";
import { revenueAvailable } from "../../query"; import { revenueAvailable } from "../../query";
import { useQueryContext } from "../../query-context";
import { useSiteContext } from "../../site-context";
function PropsModal(props) { function PropsModal({ location }) {
const {site, query, location} = props const { query } = useQueryContext();
const site = useSiteContext();
const propKey = location.pathname.split('/').filter(i => i).pop() const propKey = location.pathname.split('/').filter(i => i).pop()
/*global BUILD_EXTRA*/ /*global BUILD_EXTRA*/
@ -37,8 +39,8 @@ function PropsModal(props) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({renderLabel: (_query) => "Visitors"}), metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createEvents({renderLabel: (_query) => "Events"}), metrics.createEvents({ renderLabel: (_query) => "Events" }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage(), !hasGoalFilter(query) && metrics.createPercentage(),
showRevenueMetrics && metrics.createAverageRevenue(), showRevenueMetrics && metrics.createAverageRevenue(),
@ -47,10 +49,8 @@ function PropsModal(props) {
} }
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
getFilterInfo={getFilterInfo} getFilterInfo={getFilterInfo}
@ -60,4 +60,4 @@ function PropsModal(props) {
) )
} }
export default withRouter(withQueryContext(PropsModal)) export default withRouter(PropsModal)

View File

@ -2,14 +2,14 @@ import React, { useCallback } from "react";
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import withQueryContext from "../../components/query-context-hoc"; import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import { hasGoalFilter } from "../../util/filters";
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics"; import * as metrics from "../reports/metrics";
import { addFilter } from "../../query"; import { addFilter } from "../../query";
import { useQueryContext } from "../../query-context";
function ReferrerDrilldownModal(props) { function ReferrerDrilldownModal({ match }) {
const { site, query, match } = props const { query } = useQueryContext();
const reportInfo = { const reportInfo = {
title: "Referrer Drilldown", title: "Referrer Drilldown",
@ -33,19 +33,19 @@ function ReferrerDrilldownModal(props) {
if (hasGoalFilter(query)) { if (hasGoalFilter(query)) {
return [ return [
metrics.createTotalVisitors(), metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}), metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate() metrics.createConversionRate()
] ]
} }
if (query.period === 'realtime') { if (isRealTimeDashboard(query)) {
return [ return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'}) metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
] ]
} }
return [ return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }), metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createBounceRate(), metrics.createBounceRate(),
metrics.createVisitDuration() metrics.createVisitDuration()
] ]
@ -67,10 +67,8 @@ function ReferrerDrilldownModal(props) {
}, []) }, [])
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
getFilterInfo={getFilterInfo} getFilterInfo={getFilterInfo}
@ -82,4 +80,4 @@ function ReferrerDrilldownModal(props) {
) )
} }
export default withRouter(withQueryContext(ReferrerDrilldownModal)) export default withRouter(ReferrerDrilldownModal)

View File

@ -2,15 +2,15 @@ import React, { useCallback } from "react";
import { withRouter } from 'react-router-dom' import { withRouter } from 'react-router-dom'
import Modal from './modal' import Modal from './modal'
import withQueryContext from "../../components/query-context-hoc"; import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import { hasGoalFilter } from "../../util/filters";
import BreakdownModal from "./breakdown-modal"; import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics"; import * as metrics from "../reports/metrics";
import { addFilter } from "../../query"; import { addFilter } from "../../query";
import { useQueryContext } from "../../query-context";
const VIEWS = { const VIEWS = {
sources: { sources: {
info: {title: 'Top Sources', dimension: 'source', endpoint: '/sources', dimensionLabel: 'Source'}, info: { title: 'Top Sources', dimension: 'source', endpoint: '/sources', dimensionLabel: 'Source' },
renderIcon: (listItem) => { renderIcon: (listItem) => {
return ( return (
<img <img
@ -21,24 +21,24 @@ const VIEWS = {
} }
}, },
utm_mediums: { utm_mediums: {
info: {title: 'Top UTM Mediums', dimension: 'utm_medium', endpoint: '/utm_mediums', dimensionLabel: 'UTM Medium'} info: { title: 'Top UTM Mediums', dimension: 'utm_medium', endpoint: '/utm_mediums', dimensionLabel: 'UTM Medium' }
}, },
utm_sources: { utm_sources: {
info: {title: 'Top UTM Sources', dimension: 'utm_source', endpoint: '/utm_sources', dimensionLabel: 'UTM Source'} info: { title: 'Top UTM Sources', dimension: 'utm_source', endpoint: '/utm_sources', dimensionLabel: 'UTM Source' }
}, },
utm_campaigns: { utm_campaigns: {
info: {title: 'Top UTM Campaigns', dimension: 'utm_campaign', endpoint: '/utm_campaigns', dimensionLabel: 'UTM Campaign'} info: { title: 'Top UTM Campaigns', dimension: 'utm_campaign', endpoint: '/utm_campaigns', dimensionLabel: 'UTM Campaign' }
}, },
utm_contents: { utm_contents: {
info: {title: 'Top UTM Contents', dimension: 'utm_content', endpoint: '/utm_contents', dimensionLabel: 'UTM Content'} info: { title: 'Top UTM Contents', dimension: 'utm_content', endpoint: '/utm_contents', dimensionLabel: 'UTM Content' }
}, },
utm_terms: { utm_terms: {
info: {title: 'Top UTM Terms', dimension: 'utm_term', endpoint: '/utm_terms', dimensionLabel: 'UTM Term'} info: { title: 'Top UTM Terms', dimension: 'utm_term', endpoint: '/utm_terms', dimensionLabel: 'UTM Term' }
}, },
} }
function SourcesModal(props) { function SourcesModal({ location }) {
const { site, query, location } = props const { query } = useQueryContext();
const urlParts = location.pathname.split('/') const urlParts = location.pathname.split('/')
const currentView = urlParts[urlParts.length - 1] const currentView = urlParts[urlParts.length - 1]
@ -60,29 +60,27 @@ function SourcesModal(props) {
if (hasGoalFilter(query)) { if (hasGoalFilter(query)) {
return [ return [
metrics.createTotalVisitors(), metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}), metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate() metrics.createConversionRate()
] ]
} }
if (query.period === 'realtime') { if (isRealTimeDashboard(query)) {
return [ return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'}) metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
] ]
} }
return [ return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }), metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createBounceRate(), metrics.createBounceRate(),
metrics.createVisitDuration() metrics.createVisitDuration()
] ]
} }
return ( return (
<Modal site={site}> <Modal>
<BreakdownModal <BreakdownModal
site={site}
query={query}
reportInfo={reportInfo} reportInfo={reportInfo}
metrics={chooseMetrics()} metrics={chooseMetrics()}
getFilterInfo={getFilterInfo} getFilterInfo={getFilterInfo}
@ -93,4 +91,4 @@ function SourcesModal(props) {
) )
} }
export default withRouter(withQueryContext(SourcesModal)) export default withRouter(SourcesModal)

View File

@ -7,6 +7,8 @@ import ListReport from './../reports/list'
import * as metrics from './../reports/metrics' import * as metrics from './../reports/metrics'
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning'; import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
import { hasGoalFilter } from '../../util/filters'; import { hasGoalFilter } from '../../util/filters';
import { useQueryContext } from '../../query-context';
import { useSiteContext } from '../../site-context';
function EntryPages({ query, site, afterFetchData }) { function EntryPages({ query, site, afterFetchData }) {
function fetchData() { function fetchData() {
@ -26,7 +28,7 @@ function EntryPages({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({defaultLabel: 'Unique Entrances', meta: {plot: true}}), metrics.createVisitors({ defaultLabel: 'Unique Entrances', meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
].filter(metric => !!metric) ].filter(metric => !!metric)
} }
@ -64,7 +66,7 @@ function ExitPages({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({defaultLabel: 'Unique Exits', meta: {plot: true}}), metrics.createVisitors({ defaultLabel: 'Unique Exits', meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
].filter(metric => !!metric) ].filter(metric => !!metric)
} }
@ -102,7 +104,7 @@ function TopPages({ query, site, afterFetchData }) {
function chooseMetrics() { function chooseMetrics() {
return [ return [
metrics.createVisitors({ meta: {plot: true}}), metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(), hasGoalFilter(query) && metrics.createConversionRate(),
].filter(metric => !!metric) ].filter(metric => !!metric)
} }
@ -128,8 +130,10 @@ const labelFor = {
'exit-pages': 'Exit Pages', 'exit-pages': 'Exit Pages',
} }
export default function Pages(props) { export default function Pages() {
const { site, query } = props const { query } = useQueryContext();
const site = useSiteContext();
const tabKey = `pageTab__${site.domain}` const tabKey = `pageTab__${site.domain}`
const storedTab = storage.getItem(tabKey) const storedTab = storage.getItem(tabKey)
const [mode, setMode] = useState(storedTab || 'pages') const [mode, setMode] = useState(storedTab || 'pages')

View File

@ -3,15 +3,19 @@ import SearchTerms from './search-terms'
import SourceList from './source-list' import SourceList from './source-list'
import ReferrerList from './referrer-list' import ReferrerList from './referrer-list'
import { getFiltersByKeyPrefix, isFilteringOnFixedValue } from '../../util/filters' import { getFiltersByKeyPrefix, isFilteringOnFixedValue } from '../../util/filters'
import { useQueryContext } from '../../query-context';
import { useSiteContext } from '../../site-context';
export default function Sources(props) { export default function Sources() {
if (isFilteringOnFixedValue(props.query, 'source', 'Google')) { const { query } = useQueryContext();
return <SearchTerms {...props} /> const site = useSiteContext();
} else if (isFilteringOnFixedValue(props.query, 'source')) { if (isFilteringOnFixedValue(query, 'source', 'Google')) {
const [[_operation, _filterKey, clauses]] = getFiltersByKeyPrefix(props.query, "source") return <SearchTerms query={query} site={site} />
return <ReferrerList {...props} source={clauses[0]} /> } else if (isFilteringOnFixedValue(query, 'source')) {
} else { const [[_operation, _filterKey, clauses]] = getFiltersByKeyPrefix(query, "source")
return <SourceList {...props} /> return <ReferrerList query={query} site={site} source={clauses[0]} />
} else {
return <SourceList query={query} site={site} />
} }
} }

View File

@ -0,0 +1,14 @@
import React, { createContext, useContext } from "react";
const userContextDefaultValue = {
role: '',
loggedIn: false,
}
const UserContext = createContext(userContextDefaultValue)
export const useUserContext = () => { return useContext(UserContext) }
export default function UserContextProvider({ role, loggedIn, children }) {
return <UserContext.Provider value={{role, loggedIn}}>{children}</UserContext.Provider>
};

View File

@ -99,6 +99,10 @@ export function hasGoalFilter(query) {
return getFiltersByKeyPrefix(query, "goal").length > 0 return getFiltersByKeyPrefix(query, "goal").length > 0
} }
export function isRealTimeDashboard(query) {
return query?.period === 'realtime'
}
// Note: Currently only a single goal filter can be applied at a time. // Note: Currently only a single goal filter can be applied at a time.
export function getGoalFilter(query) { export function getGoalFilter(query) {
return getFiltersByKeyPrefix(query, "goal")[0] || null return getFiltersByKeyPrefix(query, "goal")[0] || null
@ -119,8 +123,8 @@ export function formatFilterGroup(filterGroup) {
export function cleanLabels(filters, labels, mergedFilterKey, mergedLabels) { export function cleanLabels(filters, labels, mergedFilterKey, mergedLabels) {
const filteredBy = Object.fromEntries( const filteredBy = Object.fromEntries(
filters filters
.flatMap(([_operation, filterKey, clauses]) => ['country', 'region', 'city'].includes(filterKey) ? clauses : []) .flatMap(([_operation, filterKey, clauses]) => ['country', 'region', 'city'].includes(filterKey) ? clauses : [])
.map((value) => [value, true]) .map((value) => [value, true])
) )
let result = { ...labels } let result = { ...labels }
for (const value in labels) { for (const value in labels) {

200
assets/package-lock.json generated
View File

@ -65,24 +65,27 @@
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.21.4", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/highlight": "^7.18.6" "@babel/highlight": "^7.24.7",
"picocolors": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/generator": { "node_modules/@babel/generator": {
"version": "7.21.5", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz",
"integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.21.5", "@babel/types": "^7.24.7",
"@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.17", "@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1" "jsesc": "^2.5.1"
}, },
"engines": { "engines": {
@ -90,80 +93,92 @@
} }
}, },
"node_modules/@babel/helper-environment-visitor": { "node_modules/@babel/helper-environment-visitor": {
"version": "7.21.5", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz",
"integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==",
"dev": true, "dev": true,
"license": "MIT", "dependencies": {
"@babel/types": "^7.24.7"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-function-name": { "node_modules/@babel/helper-function-name": {
"version": "7.21.0", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz",
"integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/template": "^7.20.7", "@babel/template": "^7.24.7",
"@babel/types": "^7.21.0" "@babel/types": "^7.24.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-hoist-variables": { "node_modules/@babel/helper-hoist-variables": {
"version": "7.18.6", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz",
"integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.18.6" "@babel/types": "^7.24.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-split-export-declaration": { "node_modules/@babel/helper-split-export-declaration": {
"version": "7.18.6", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
"integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.18.6" "@babel/types": "^7.24.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.21.5", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
"integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.19.1", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/highlight": { "node_modules/@babel/highlight": {
"version": "7.18.6", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
"integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.18.6", "@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.0.0", "chalk": "^2.4.2",
"js-tokens": "^4.0.0" "js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.21.8", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
"integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
}, },
@ -182,32 +197,34 @@
} }
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.20.7", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.18.6", "@babel/code-frame": "^7.24.7",
"@babel/parser": "^7.20.7", "@babel/parser": "^7.24.7",
"@babel/types": "^7.20.7" "@babel/types": "^7.24.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/traverse": { "node_modules/@babel/traverse": {
"version": "7.21.5", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz",
"integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.21.4", "@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.21.5", "@babel/generator": "^7.24.7",
"@babel/helper-environment-visitor": "^7.21.5", "@babel/helper-environment-visitor": "^7.24.7",
"@babel/helper-function-name": "^7.21.0", "@babel/helper-function-name": "^7.24.7",
"@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-hoist-variables": "^7.24.7",
"@babel/helper-split-export-declaration": "^7.18.6", "@babel/helper-split-export-declaration": "^7.24.7",
"@babel/parser": "^7.21.5", "@babel/parser": "^7.24.7",
"@babel/types": "^7.21.5", "@babel/types": "^7.24.7",
"debug": "^4.1.0", "debug": "^4.3.1",
"globals": "^11.1.0" "globals": "^11.1.0"
}, },
"engines": { "engines": {
@ -215,12 +232,13 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.21.5", "version": "7.24.7",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz",
"integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.21.5", "@babel/helper-string-parser": "^7.24.7",
"@babel/helper-validator-identifier": "^7.19.1", "@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
}, },
"engines": { "engines": {
@ -311,41 +329,46 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@jridgewell/gen-mapping": { "node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2", "version": "0.3.5",
"license": "MIT", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dependencies": { "dependencies": {
"@jridgewell/set-array": "^1.0.1", "@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9" "@jridgewell/trace-mapping": "^0.3.24"
}, },
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0", "version": "3.1.2",
"license": "MIT", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/set-array": { "node_modules/@jridgewell/set-array": {
"version": "1.1.2", "version": "1.2.1",
"license": "MIT", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": { "engines": {
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14", "version": "1.4.15",
"license": "MIT" "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
}, },
"node_modules/@jridgewell/trace-mapping": { "node_modules/@jridgewell/trace-mapping": {
"version": "0.3.18", "version": "0.3.25",
"license": "MIT", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": { "dependencies": {
"@jridgewell/resolve-uri": "3.1.0", "@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@jsonurl/jsonurl": { "node_modules/@jsonurl/jsonurl": {
@ -740,10 +763,11 @@
} }
}, },
"node_modules/braces": { "node_modules/braces": {
"version": "3.0.2", "version": "3.0.3",
"license": "MIT", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.1.1"
}, },
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -1999,8 +2023,9 @@
} }
}, },
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.0.1", "version": "7.1.1",
"license": "MIT", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@ -2652,7 +2677,8 @@
}, },
"node_modules/is-number": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"license": "MIT", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@ -2846,8 +2872,9 @@
}, },
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true, "dev": true,
"license": "MIT",
"bin": { "bin": {
"jsesc": "bin/jsesc" "jsesc": "bin/jsesc"
}, },
@ -3116,14 +3143,15 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.6", "version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"bin": { "bin": {
"nanoid": "bin/nanoid.cjs" "nanoid": "bin/nanoid.cjs"
}, },
@ -3461,8 +3489,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.1",
"license": "ISC" "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@ -3491,7 +3520,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.29", "version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@ -3506,11 +3537,10 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.6", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.1",
"source-map-js": "^1.0.2" "source-map-js": "^1.2.0"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@ -4246,8 +4276,9 @@
} }
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.0.2", "version": "1.2.0",
"license": "BSD-3-Clause", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -4839,7 +4870,8 @@
}, },
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"license": "MIT", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },