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",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"prettier"
],
"parser": "babel-eslint",

View File

@ -7,36 +7,16 @@ import ErrorBoundary from './dashboard/error-boundary'
import * as api from './dashboard/api'
import * as timer from './dashboard/util/realtime-update-timer'
import { filtersBackwardsCompatibilityRedirect } from './dashboard/query';
import SiteContextProvider, { parseSiteFromDataset } from './dashboard/site-context';
import UserContextProvider from './dashboard/user-context'
timer.start()
const container = document.getElementById('stats-react-container')
if (container) {
const site = {
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 site = parseSiteFromDataset(container.dataset)
const loggedIn = container.dataset.loggedIn === 'true'
const currentUserRole = container.dataset.currentUserRole
const sharedLinkAuth = container.dataset.sharedLinkAuth
if (sharedLinkAuth) {
api.setSharedLinkAuth(sharedLinkAuth)
@ -46,7 +26,11 @@ if (container) {
const app = (
<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>
)

View File

@ -7,6 +7,8 @@ import classNames from 'classnames'
import * as storage from './util/storage'
import Flatpickr from 'react-flatpickr'
import { parseNaiveDate, formatISO, formatDateRange } from './util/date.js'
import { useQueryContext } from './query-context.js'
import { useSiteContext } from './site-context.js'
const COMPARISON_MODES = {
'off': 'Disable comparison',
@ -19,11 +21,11 @@ const DEFAULT_COMPARISON_MODE = 'previous_period'
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'
}
export const getStoredComparisonMode = function(domain) {
export const getStoredComparisonMode = function (domain) {
const mode = storage.getItem(`comparison_mode__${domain}`)
if (Object.keys(COMPARISON_MODES).includes(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
storage.setItem(`comparison_mode__${domain}`, mode)
}
export const isComparisonEnabled = function(mode) {
export const isComparisonEnabled = function (mode) {
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 (isComparisonEnabled(query.comparison)) {
@ -72,14 +74,14 @@ function ComparisonModeOption({ label, value, isCurrentlySelected, updateMode, s
"font-bold": isCurrentlySelected,
})
return <button className={buttonClass}>{ label }</button>
return <button className={buttonClass}>{label}</button>
}
const disabled = isCurrentlySelected && value !== "custom"
return (
<Menu.Item key={value} onClick={click} disabled={disabled}>
{ render }
{render}
</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 (!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)
// eslint-disable-next-line react-hooks/rules-of-hooks
const [uiMode, setUiMode] = React.useState("menu")
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
if (uiMode == "datepicker") {
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">
<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">
<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" />
</Menu.Button>
<Transition
@ -174,18 +181,18 @@ const ComparisonInput = function({ site, query, history }) {
leaveFrom="opacity-100 scale-100"
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>
{ Object.keys(COMPARISON_MODES).map((key) => ComparisonModeOption({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode, setUiMode })) }
{ query.comparison !== "custom" && <span>
{Object.keys(COMPARISON_MODES).map((key) => ComparisonModeOption({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode, setUiMode }))}
{query.comparison !== "custom" && <span>
<hr className="my-1" />
<MatchDayOfWeekInput query={query} history={history} site={site} />
</span>}
</Menu.Items>
</Transition>
{ uiMode == "datepicker" &&
{uiMode == "datepicker" &&
<div className="h-0 md:absolute">
<Flatpickr ref={calendar} options={flatpickrOptions} className="invisible" />
</div> }
</div>}
</Menu>
</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 { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js"
import classNames from "classnames"
import { useQueryContext } from "./query-context.js";
import { useSiteContext } from "./site-context.js";
function renderArrow(query, site, period, prevDate, nextDate) {
const insertionDate = parseUTCDate(site.statsBegin);
@ -156,7 +158,9 @@ function DisplayPeriod({ query, site }) {
return 'Realtime'
}
function DatePicker({ query, site, history }) {
function DatePicker({ history }) {
const { query } = useQueryContext();
const site = useSiteContext();
const [open, setOpen] = useState(false)
const [mode, setMode] = useState('menu')
const dropDownNode = useRef(null)

View File

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

View File

@ -13,45 +13,51 @@ import Behaviours from './stats/behaviours'
import ComparisonInput from './comparison-input'
import { withPinnedHeader } from './pinned-header-hoc';
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)
return (
<div className="mb-12">
<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="flex items-center w-full" ref={tooltipBoundary}>
<SiteSwitcher site={props.site} loggedIn={props.loggedIn} currentUserRole={props.currentUserRole} />
<CurrentVisitors site={props.site} query={props.query} lastLoadTimestamp={props.lastLoadTimestamp} tooltipBoundary={tooltipBoundary.current} />
<Filters className="flex" site={props.site} query={props.query} history={props.history} />
<SiteSwitcher site={site} loggedIn={user.loggedIn} currentUserRole={user.role} />
<CurrentVisitors tooltipBoundary={tooltipBoundary.current} />
<Filters className="flex" />
</div>
<Datepicker site={props.site} query={props.query} />
<ComparisonInput site={props.site} query={props.query} />
<Datepicker />
<ComparisonInput />
</div>
</div>
<VisitorGraph site={props.site} query={props.query} updateImportedDataInView={props.updateImportedDataInView}/>
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
<div className="w-full md:flex">
<div className={ statsBoxClass }>
<Sources site={props.site} query={props.query} />
<div className={statsBoxClass}>
<Sources />
</div>
<div className={ statsBoxClass }>
<Pages site={props.site} query={props.query} />
<div className={statsBoxClass}>
<Pages />
</div>
</div>
<div className="w-full md:flex">
<div className={ statsBoxClass }>
<Locations site={props.site} query={props.query} />
<div className={statsBoxClass}>
<Locations site={site} query={query} />
</div>
<div className={ statsBoxClass }>
<Devices site={props.site} query={props.query} />
<div className={statsBoxClass}>
<Devices />
</div>
</div>
<Behaviours site={props.site} query={props.query} currentUserRole={props.currentUserRole} importedDataInView={props.importedDataInView}/>
<Behaviours importedDataInView={importedDataInView} />
</div>
)
}

View File

@ -1,46 +1,26 @@
import React from 'react'
import { withRouter } from 'react-router-dom'
import React, { useState } from 'react';
import Historical from './historical'
import Realtime from './realtime'
import withQueryContext from './components/query-context-hoc';
import Realtime, { useIsRealtimeDashboard } from './realtime'
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) {
const {
site,
loggedIn,
currentUserRole,
query,
importedDataInView,
updateImportedDataInView,
lastLoadTimestamp
} = props
export function Dashboard() {
const isRealTimeDashboard = useIsRealtimeDashboard();
const [importedDataInView, setImportedDataInView] = useState(false)
if (query.period === 'realtime') {
if (isRealTimeDashboard) {
return (
<Realtime
site={site}
loggedIn={loggedIn}
currentUserRole={currentUserRole}
query={query}
lastLoadTimestamp={lastLoadTimestamp}
/>
<Realtime />
)
} else {
return (
<Historical
site={site}
loggedIn={loggedIn}
currentUserRole={currentUserRole}
query={query}
lastLoadTimestamp={lastLoadTimestamp}
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 SiteSwitcher from './site-switcher'
@ -11,9 +11,20 @@ import Devices from './stats/devices'
import Behaviours from './stats/behaviours'
import { withPinnedHeader } from './pinned-header-hoc';
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) {
const {site, query, history, stuck, loggedIn, currentUserRole, lastLoadTimestamp} = props
export const useIsRealtimeDashboard = () => {
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'
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="items-center w-full flex">
<div className="flex items-center w-full">
<SiteSwitcher site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
<Filters className="flex" site={site} query={query} history={history} />
<SiteSwitcher site={site} loggedIn={user.loggedIn} currentUserRole={user.role} />
<Filters className="flex" />
</div>
<Datepicker site={site} query={query} />
<Datepicker />
</div>
</div>
<VisitorGraph site={site} query={query} lastLoadTimestamp={lastLoadTimestamp}/>
<VisitorGraph />
<div className="w-full md:flex">
<div className={ statsBoxClass }>
<Sources site={site} query={query} />
<div className={statsBoxClass}>
<Sources />
</div>
<div className={ statsBoxClass }>
<Pages site={site} query={query} />
<div className={statsBoxClass}>
<Pages />
</div>
</div>
<div className="w-full md:flex">
<div className={ statsBoxClass }>
<div className={statsBoxClass}>
<Locations site={site} query={query} />
</div>
<div className={ statsBoxClass }>
<Devices site={site} query={query} />
<div className={statsBoxClass}>
<Devices />
</div>
</div>
<Behaviours site={site} query={query} currentUserRole={currentUserRole} />
<Behaviours />
</div>
)
}

View File

@ -1,7 +1,7 @@
import React, { useEffect } from 'react';
import { BrowserRouter, Switch, Route, useLocation } from "react-router-dom";
import Dash from './index'
import Dashboard from './index'
import SourcesModal from './stats/modals/sources'
import ReferrersDrilldownModal from './stats/modals/referrer-drilldown'
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 ConversionsModal from './stats/modals/conversions'
import FilterModal from './stats/modals/filter-modal'
import QueryContextProvider from './query-context';
import { useSiteContext } from './site-context';
function ScrollToTop() {
const location = useLocation();
@ -25,45 +27,48 @@ function ScrollToTop() {
return null;
}
export default function Router({ site, loggedIn, currentUserRole }) {
export default function Router() {
const site = useSiteContext()
return (
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
<QueryContextProvider>
<Route path="/">
<ScrollToTop />
<Dash site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
<Dashboard />
<Switch>
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
<SourcesModal site={site} />
<SourcesModal />
</Route>
<Route exact path="/referrers/Google">
<GoogleKeywordsModal site={site} />
</Route>
<Route exact path="/referrers/:referrer">
<ReferrersDrilldownModal site={site} />
<ReferrersDrilldownModal />
</Route>
<Route path="/pages">
<PagesModal site={site} />
<PagesModal />
</Route>
<Route path="/entry-pages">
<EntryPagesModal site={site} />
<EntryPagesModal />
</Route>
<Route path="/exit-pages">
<ExitPagesModal site={site} />
<ExitPagesModal />
</Route>
<Route exact path={["/countries", "/regions", "/cities"]}>
<LocationsModal site={site} />
<LocationsModal />
</Route>
<Route path="/custom-prop-values/:prop_key">
<PropsModal site={site} />
<PropsModal />
</Route>
<Route path="/conversions">
<ConversionsModal site={site} />
<ConversionsModal />
</Route>
<Route path={["/filter/:field"]}>
<FilterModal site={site} />
</Route>
</Switch>
</Route>
</BrowserRouter >
</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 { SPECIAL_GOALS } from './goal-conversions'
import { hasGoalFilter } from '../../util/filters'
import { useSiteContext } from '../../site-context'
import { useQueryContext } from '../../query-context'
import { useUserContext } from '../../user-context'
/*global BUILD_EXTRA*/
/*global require*/
@ -35,9 +38,12 @@ export const sectionTitles = {
[FUNNELS]: 'Funnels'
}
export default function Behaviours(props) {
const { site, query, currentUserRole } = props
const adminAccess = ['owner', 'admin', 'super_admin'].includes(currentUserRole)
export default function Behaviours({ importedDataInView }) {
const { query } = useQueryContext();
const site = useSiteContext();
const user = useUserContext();
const adminAccess = ['owner', 'admin', 'super_admin'].includes(user.role)
const tabKey = `behavioursTab__${site.domain}`
const funnelKey = `behavioursTabFunnel__${site.domain}`
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,
// it only makes sense to display the feature tab to the owner itself
// as only they can upgrade to make the feature available.
const callToActionIsMissing = !isAvailable && currentUserRole !== 'owner'
const callToActionIsMissing = !isAvailable && user.role !== 'owner'
if (!isOptedOut && !callToActionIsMissing) {
enabledModes.push(feature)
@ -341,7 +347,7 @@ export default function Behaviours(props) {
} else if (mode === PROPS) {
return <ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason} message="Imported data is unavailable in this view" />
} 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 { Tooltip } from '../util/tooltip';
import { SecondsSinceLastLoad } from '../util/seconds-since-last-load';
import { useQueryContext } from '../query-context';
import { useSiteContext } from '../site-context';
export default function CurrentVisitors(props) {
const { site, query, lastLoadTimestamp, tooltipBoundary } = props
export default function CurrentVisitors({ tooltipBoundary }) {
const { query, lastLoadTimestamp } = useQueryContext();
const site = useSiteContext();
const [currentVisitors, setCurrentVisitors] = useState(null)
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 { getFiltersByKeyPrefix, hasGoalFilter, isFilteringOnFixedValue } from '../../util/filters'
import ListReport from '../reports/list'
@ -6,6 +6,8 @@ import * as metrics from '../reports/metrics'
import * as api from '../../api'
import * as url from '../../util/url'
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
const BROWSER_ICONS = {
@ -57,7 +59,7 @@ function Browsers({ query, site, afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: {plot: true}}),
metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric)
@ -80,9 +82,11 @@ function BrowserVersions({ query, site, afterFetchData }) {
function fetchData() {
return api.get(url.apiPath(site, '/browser-versions'), query)
.then(res => {
return {...res, results: res.results.map((row => {
return {...row, name: `${row.browser} ${row.name}`, version: row.name}
}))}
return {
...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() {
return [
metrics.createVisitors({ meta: {plot: true}}),
metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric)
@ -166,9 +170,9 @@ function OperatingSystems({ query, site, afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: {plot: true}}),
metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage({meta: {hiddenonMobile: true}})
!hasGoalFilter(query) && metrics.createPercentage({ meta: { hiddenonMobile: true } })
].filter(metric => !!metric)
}
@ -193,9 +197,11 @@ function OperatingSystemVersions({ query, site, afterFetchData }) {
function fetchData() {
return api.get(url.apiPath(site, '/operating-system-versions'), query)
.then(res => {
return {...res, results: res.results.map((row => {
return {...row, name: `${row.os} ${row.name}`, version: row.name}
}))}
return {
...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() {
return [
metrics.createVisitors({ meta: {plot: true}}),
metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric)
@ -255,7 +261,7 @@ function ScreenSizes({ query, site, afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: {plot: true}}),
metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage()
].filter(metric => !!metric)
@ -296,8 +302,10 @@ function iconFor(screenSize) {
}
}
export default function Devices(props) {
const {site, query} = props
export default function Devices() {
const { query } = useQueryContext();
const site = useSiteContext();
const tabKey = `deviceTab__${site.domain}`
const storedTab = storage.getItem(tabKey)
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 gap-x-1">
<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 className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
{renderPill('Browser', 'browser')}

View File

@ -64,10 +64,12 @@ function storeInterval(period, domain, interval) {
}
function subscribeKeybinding(element) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const handleKeyPress = useCallback((event) => {
if (isKeyPressed(event, "i")) element.current?.click()
}, [])
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
document.addEventListener('keydown', handleKeyPress)
return () => document.removeEventListener('keydown', handleKeyPress)
@ -90,6 +92,7 @@ export const getCurrentInterval = function(site, query) {
export function IntervalPicker({ query, site, onIntervalUpdate }) {
if (query.period == 'realtime') return null
// eslint-disable-next-line react-hooks/rules-of-hooks
const menuElement = React.useRef(null)
const options = validIntervals(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 { isComparisonEnabled } from '../../comparison-input'
import LineGraphWithRouter from './line-graph'
import { useQueryContext } from '../../query-context';
import { useSiteContext } from '../../site-context';
function fetchTopStats(site, query) {
const q = { ...query }
@ -23,12 +25,14 @@ function fetchTopStats(site, query) {
}
function fetchMainGraph(site, query, metric, interval) {
const params = {metric, interval}
const params = { metric, interval }
return api.get(url.apiPath(site, '/main-graph'), query, params)
}
export default function VisitorGraph(props) {
const {site, query, lastLoadTimestamp} = props
export default function VisitorGraph({ updateImportedDataInView }) {
const { query, lastLoadTimestamp } = useQueryContext();
const site = useSiteContext();
const isRealtime = query.period === 'realtime'
const isDarkTheme = document.querySelector('html').classList.contains('dark') || false
@ -81,8 +85,8 @@ export default function VisitorGraph(props) {
function fetchTopStatsAndGraphData() {
fetchTopStats(site, query)
.then((res) => {
if (props.updateImportedDataInView) {
props.updateImportedDataInView(res.includes_imported)
if (updateImportedDataInView) {
updateImportedDataInView(res.includes_imported)
}
setTopStatData(res)
setTopStatsLoading(false)
@ -149,7 +153,7 @@ export default function VisitorGraph(props) {
{graphRefreshing && renderLoader()}
<div className="absolute right-4 -top-8 py-1 flex items-center">
{!isRealtime && <StatsExport site={site} query={query} />}
<SamplingNotice samplePercent={topStatData}/>
<SamplingNotice samplePercent={topStatData} />
<WithImportedSwitch query={query} info={topStatData && topStatData.with_imported_switch} />
<IntervalPicker site={site} query={query} onIntervalUpdate={onIntervalUpdate} />
</div>

View File

@ -4,6 +4,8 @@ import * as api from '../../api'
import { useDebouncedEffect, useMountedEffect } from '../../custom-hooks'
import { trimURL } from '../../util/url'
import { FilterLink } from "../reports/list";
import { useQueryContext } from "../../query-context";
import { useSiteContext } from "../../site-context";
const LIMIT = 100
const MIN_HEIGHT_PX = 500
@ -33,11 +35,6 @@ const MIN_HEIGHT_PX = 500
// ### 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:
// * `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`,
// but will be called after a successful next page load in `fetchNextPage`.
export default function BreakdownModal({
site,
query,
reportInfo,
metrics,
renderIcon,
@ -89,6 +84,9 @@ export default function BreakdownModal({
addSearchFilter,
getFilterInfo
}) {
const {query} = useQueryContext();
const site = useSiteContext();
const endpoint = `/api/stats/${encodeURIComponent(site.domain)}${reportInfo.endpoint}`
const [initialLoading, setInitialLoading] = useState(true)

View File

@ -1,15 +1,11 @@
import React, { useCallback, useState } from "react";
import { withRouter } from 'react-router-dom'
import Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics";
/*global BUILD_EXTRA*/
function ConversionsModal(props) {
const { site, query } = props
function ConversionsModal() {
const [showRevenue, setShowRevenue] = useState(false)
const reportInfo = {
@ -28,8 +24,8 @@ function ConversionsModal(props) {
function chooseMetrics() {
return [
metrics.createVisitors({renderLabel: (_query) => "Uniques"}),
metrics.createEvents({renderLabel: (_query) => "Total"}),
metrics.createVisitors({ renderLabel: (_query) => "Uniques" }),
metrics.createEvents({ renderLabel: (_query) => "Total" }),
metrics.createConversionRate(),
showRevenue && metrics.createAverageRevenue(),
showRevenue && metrics.createTotalRevenue(),
@ -54,10 +50,8 @@ function ConversionsModal(props) {
}
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 { withRouter } from 'react-router-dom'
import React, { useCallback } from "react";
import Modal from './modal'
import { hasGoalFilter } from "../../util/filters";
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import { addFilter } from '../../query'
import BreakdownModal from "./breakdown-modal";
import * as metrics from '../reports/metrics'
import withQueryContext from "../../components/query-context-hoc";
import { useQueryContext } from "../../query-context";
function EntryPagesModal(props) {
const { site, query } = props
function EntryPagesModal() {
const { query } = useQueryContext();
const reportInfo = {
title: 'Entry Pages',
@ -32,29 +31,27 @@ function EntryPagesModal(props) {
if (hasGoalFilter(query)) {
return [
metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate()
]
}
if (query.period === 'realtime') {
if (isRealTimeDashboard(query)) {
return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
]
}
return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
metrics.createVisits({renderLabel: (_query) => "Total Entrances" }),
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createVisits({ renderLabel: (_query) => "Total Entrances" }),
metrics.createVisitDuration()
]
}
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 { withRouter } from 'react-router-dom'
import React, { useCallback } from "react";
import Modal from './modal'
import { hasGoalFilter } from "../../util/filters";
import { addFilter } from '../../query'
import BreakdownModal from "./breakdown-modal";
import * as metrics from '../reports/metrics'
import withQueryContext from "../../components/query-context-hoc";
import { useQueryContext } from "../../query-context";
function ExitPagesModal(props) {
const { site, query } = props
function ExitPagesModal() {
const { query } = useQueryContext();
const reportInfo = {
title: 'Exit Pages',
@ -32,29 +31,27 @@ function ExitPagesModal(props) {
if (hasGoalFilter(query)) {
return [
metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate()
]
}
if (query.period === 'realtime') {
return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
]
}
return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
metrics.createVisits({renderLabel: (_query) => "Total Exits" }),
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createVisits({ renderLabel: (_query) => "Total Exits" }),
metrics.createExitRate()
]
}
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import { hasGoalFilter } from "../../util/filters";
import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics";
import { useQueryContext } from "../../query-context";
const VIEWS = {
countries: {title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country'},
regions: {title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region'},
cities: {title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City'},
countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country' },
regions: { title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region' },
cities: { title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City' },
}
function LocationsModal(props) {
const { site, query, location } = props
function LocationsModal({ location }) {
const { query } = useQueryContext();
const urlParts = location.pathname.split('/')
const currentView = urlParts[urlParts.length - 1]
@ -32,19 +32,19 @@ function LocationsModal(props) {
if (hasGoalFilter(query)) {
return [
metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate()
]
}
if (query.period === 'realtime') {
return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
]
}
return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
currentView === 'countries' && metrics.createPercentage()
].filter(metric => !!metric)
}
@ -56,10 +56,8 @@ function LocationsModal(props) {
}, [])
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 { withRouter } from 'react-router-dom'
import Modal from './modal'
import { hasGoalFilter } from "../../util/filters";
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import { addFilter } from '../../query'
import BreakdownModal from "./breakdown-modal";
import * as metrics from '../reports/metrics'
import withQueryContext from "../../components/query-context-hoc";
import { useQueryContext } from "../../query-context";
function PagesModal(props) {
const { site, query } = props
function PagesModal() {
const { query } = useQueryContext();
const reportInfo = {
title: 'Top Pages',
@ -37,7 +36,7 @@ function PagesModal(props) {
]
}
if (query.period === 'realtime') {
if (isRealTimeDashboard(query)) {
return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
]
@ -52,10 +51,8 @@ function PagesModal(props) {
}
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import { addFilter } from '../../query'
import { specialTitleWhenGoalFilter } from "../behaviours/goal-conversions";
import { EVENT_PROPS_PREFIX, hasGoalFilter } from "../../util/filters"
import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics";
import { revenueAvailable } from "../../query";
import { useQueryContext } from "../../query-context";
import { useSiteContext } from "../../site-context";
function PropsModal(props) {
const {site, query, location} = props
function PropsModal({ location }) {
const { query } = useQueryContext();
const site = useSiteContext();
const propKey = location.pathname.split('/').filter(i => i).pop()
/*global BUILD_EXTRA*/
@ -37,8 +39,8 @@ function PropsModal(props) {
function chooseMetrics() {
return [
metrics.createVisitors({renderLabel: (_query) => "Visitors"}),
metrics.createEvents({renderLabel: (_query) => "Events"}),
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createEvents({ renderLabel: (_query) => "Events" }),
hasGoalFilter(query) && metrics.createConversionRate(),
!hasGoalFilter(query) && metrics.createPercentage(),
showRevenueMetrics && metrics.createAverageRevenue(),
@ -47,10 +49,8 @@ function PropsModal(props) {
}
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import { hasGoalFilter } from "../../util/filters";
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics";
import { addFilter } from "../../query";
import { useQueryContext } from "../../query-context";
function ReferrerDrilldownModal(props) {
const { site, query, match } = props
function ReferrerDrilldownModal({ match }) {
const { query } = useQueryContext();
const reportInfo = {
title: "Referrer Drilldown",
@ -33,19 +33,19 @@ function ReferrerDrilldownModal(props) {
if (hasGoalFilter(query)) {
return [
metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate()
]
}
if (query.period === 'realtime') {
if (isRealTimeDashboard(query)) {
return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
]
}
return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createBounceRate(),
metrics.createVisitDuration()
]
@ -67,10 +67,8 @@ function ReferrerDrilldownModal(props) {
}, [])
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 Modal from './modal'
import withQueryContext from "../../components/query-context-hoc";
import { hasGoalFilter } from "../../util/filters";
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
import BreakdownModal from "./breakdown-modal";
import * as metrics from "../reports/metrics";
import { addFilter } from "../../query";
import { useQueryContext } from "../../query-context";
const VIEWS = {
sources: {
info: {title: 'Top Sources', dimension: 'source', endpoint: '/sources', dimensionLabel: 'Source'},
info: { title: 'Top Sources', dimension: 'source', endpoint: '/sources', dimensionLabel: 'Source' },
renderIcon: (listItem) => {
return (
<img
@ -21,24 +21,24 @@ const VIEWS = {
}
},
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: {
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: {
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: {
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: {
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) {
const { site, query, location } = props
function SourcesModal({ location }) {
const { query } = useQueryContext();
const urlParts = location.pathname.split('/')
const currentView = urlParts[urlParts.length - 1]
@ -60,29 +60,27 @@ function SourcesModal(props) {
if (hasGoalFilter(query)) {
return [
metrics.createTotalVisitors(),
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
metrics.createConversionRate()
]
}
if (query.period === 'realtime') {
if (isRealTimeDashboard(query)) {
return [
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
]
}
return [
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
metrics.createBounceRate(),
metrics.createVisitDuration()
]
}
return (
<Modal site={site}>
<Modal>
<BreakdownModal
site={site}
query={query}
reportInfo={reportInfo}
metrics={chooseMetrics()}
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 ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
import { hasGoalFilter } from '../../util/filters';
import { useQueryContext } from '../../query-context';
import { useSiteContext } from '../../site-context';
function EntryPages({ query, site, afterFetchData }) {
function fetchData() {
@ -26,7 +28,7 @@ function EntryPages({ query, site, afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({defaultLabel: 'Unique Entrances', meta: {plot: true}}),
metrics.createVisitors({ defaultLabel: 'Unique Entrances', meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
].filter(metric => !!metric)
}
@ -64,7 +66,7 @@ function ExitPages({ query, site, afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({defaultLabel: 'Unique Exits', meta: {plot: true}}),
metrics.createVisitors({ defaultLabel: 'Unique Exits', meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
].filter(metric => !!metric)
}
@ -102,7 +104,7 @@ function TopPages({ query, site, afterFetchData }) {
function chooseMetrics() {
return [
metrics.createVisitors({ meta: {plot: true}}),
metrics.createVisitors({ meta: { plot: true } }),
hasGoalFilter(query) && metrics.createConversionRate(),
].filter(metric => !!metric)
}
@ -128,8 +130,10 @@ const labelFor = {
'exit-pages': 'Exit Pages',
}
export default function Pages(props) {
const { site, query } = props
export default function Pages() {
const { query } = useQueryContext();
const site = useSiteContext();
const tabKey = `pageTab__${site.domain}`
const storedTab = storage.getItem(tabKey)
const [mode, setMode] = useState(storedTab || 'pages')

View File

@ -3,15 +3,19 @@ import SearchTerms from './search-terms'
import SourceList from './source-list'
import ReferrerList from './referrer-list'
import { getFiltersByKeyPrefix, isFilteringOnFixedValue } from '../../util/filters'
import { useQueryContext } from '../../query-context';
import { useSiteContext } from '../../site-context';
export default function Sources(props) {
if (isFilteringOnFixedValue(props.query, 'source', 'Google')) {
return <SearchTerms {...props} />
} else if (isFilteringOnFixedValue(props.query, 'source')) {
const [[_operation, _filterKey, clauses]] = getFiltersByKeyPrefix(props.query, "source")
return <ReferrerList {...props} source={clauses[0]} />
export default function Sources() {
const { query } = useQueryContext();
const site = useSiteContext();
if (isFilteringOnFixedValue(query, 'source', 'Google')) {
return <SearchTerms query={query} site={site} />
} else if (isFilteringOnFixedValue(query, 'source')) {
const [[_operation, _filterKey, clauses]] = getFiltersByKeyPrefix(query, "source")
return <ReferrerList query={query} site={site} source={clauses[0]} />
} else {
return <SourceList {...props} />
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
}
export function isRealTimeDashboard(query) {
return query?.period === 'realtime'
}
// Note: Currently only a single goal filter can be applied at a time.
export function getGoalFilter(query) {
return getFiltersByKeyPrefix(query, "goal")[0] || null

200
assets/package-lock.json generated
View File

@ -65,24 +65,27 @@
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/highlight": "^7.18.6"
"@babel/highlight": "^7.24.7",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.21.5",
"@jridgewell/gen-mapping": "^0.3.2",
"@jridgewell/trace-mapping": "^0.3.17",
"@babel/types": "^7.24.7",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^2.5.1"
},
"engines": {
@ -90,80 +93,92 @@
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.20.7",
"@babel/types": "^7.21.0"
"@babel/template": "^7.24.7",
"@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.18.6"
"@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.18.6",
"chalk": "^2.0.0",
"js-tokens": "^4.0.0"
"@babel/helper-validator-identifier": "^7.24.7",
"chalk": "^2.4.2",
"js-tokens": "^4.0.0",
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"bin": {
"parser": "bin/babel-parser.js"
},
@ -182,32 +197,34 @@
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.18.6",
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7"
"@babel/code-frame": "^7.24.7",
"@babel/parser": "^7.24.7",
"@babel/types": "^7.24.7"
},
"engines": {
"node": ">=6.9.0"
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.21.4",
"@babel/generator": "^7.21.5",
"@babel/helper-environment-visitor": "^7.21.5",
"@babel/helper-function-name": "^7.21.0",
"@babel/helper-hoist-variables": "^7.18.6",
"@babel/helper-split-export-declaration": "^7.18.6",
"@babel/parser": "^7.21.5",
"@babel/types": "^7.21.5",
"debug": "^4.1.0",
"@babel/code-frame": "^7.24.7",
"@babel/generator": "^7.24.7",
"@babel/helper-environment-visitor": "^7.24.7",
"@babel/helper-function-name": "^7.24.7",
"@babel/helper-hoist-variables": "^7.24.7",
"@babel/helper-split-export-declaration": "^7.24.7",
"@babel/parser": "^7.24.7",
"@babel/types": "^7.24.7",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
@ -215,12 +232,13 @@
}
},
"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,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.21.5",
"@babel/helper-validator-identifier": "^7.19.1",
"@babel/helper-string-parser": "^7.24.7",
"@babel/helper-validator-identifier": "^7.24.7",
"to-fast-properties": "^2.0.0"
},
"engines": {
@ -311,41 +329,46 @@
"license": "BSD-3-Clause"
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.2",
"license": "MIT",
"version": "0.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
"dependencies": {
"@jridgewell/set-array": "^1.0.1",
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.9"
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.0",
"license": "MIT",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.1.2",
"license": "MIT",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.4.14",
"license": "MIT"
"version": "1.4.15",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.18",
"license": "MIT",
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"dependencies": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@jsonurl/jsonurl": {
@ -740,10 +763,11 @@
}
},
"node_modules/braces": {
"version": "3.0.2",
"license": "MIT",
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"dependencies": {
"fill-range": "^7.0.1"
"fill-range": "^7.1.1"
},
"engines": {
"node": ">=8"
@ -1999,8 +2023,9 @@
}
},
"node_modules/fill-range": {
"version": "7.0.1",
"license": "MIT",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"dependencies": {
"to-regex-range": "^5.0.1"
},
@ -2652,7 +2677,8 @@
},
"node_modules/is-number": {
"version": "7.0.0",
"license": "MIT",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"engines": {
"node": ">=0.12.0"
}
@ -2846,8 +2872,9 @@
},
"node_modules/jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
@ -3116,14 +3143,15 @@
}
},
"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": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@ -3461,8 +3489,9 @@
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.0",
"license": "ISC"
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
},
"node_modules/picomatch": {
"version": "2.3.1",
@ -3491,7 +3520,9 @@
}
},
"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": [
{
"type": "opencollective",
@ -3506,11 +3537,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.6",
"picocolors": "^1.0.0",
"source-map-js": "^1.0.2"
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -4246,8 +4276,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.0.2",
"license": "BSD-3-Clause",
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"engines": {
"node": ">=0.10.0"
}
@ -4839,7 +4870,8 @@
},
"node_modules/to-regex-range": {
"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": {
"is-number": "^7.0.0"
},