mirror of
https://github.com/plausible/analytics.git
synced 2024-12-24 01:54:34 +03:00
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:
parent
645b81376c
commit
2dd2f058d1
@ -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",
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
40
assets/js/dashboard/query-context.js
Normal file
40
assets/js/dashboard/query-context.js
Normal 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);
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)}>
|
||||||
|
<QueryContextProvider>
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
<Dash site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
|
<Dashboard />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
|
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
|
||||||
<SourcesModal site={site} />
|
<SourcesModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/referrers/Google">
|
<Route exact path="/referrers/Google">
|
||||||
<GoogleKeywordsModal site={site} />
|
<GoogleKeywordsModal site={site} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path="/referrers/:referrer">
|
<Route exact path="/referrers/:referrer">
|
||||||
<ReferrersDrilldownModal site={site} />
|
<ReferrersDrilldownModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/pages">
|
<Route path="/pages">
|
||||||
<PagesModal site={site} />
|
<PagesModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/entry-pages">
|
<Route path="/entry-pages">
|
||||||
<EntryPagesModal site={site} />
|
<EntryPagesModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/exit-pages">
|
<Route path="/exit-pages">
|
||||||
<ExitPagesModal site={site} />
|
<ExitPagesModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route exact path={["/countries", "/regions", "/cities"]}>
|
<Route exact path={["/countries", "/regions", "/cities"]}>
|
||||||
<LocationsModal site={site} />
|
<LocationsModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/custom-prop-values/:prop_key">
|
<Route path="/custom-prop-values/:prop_key">
|
||||||
<PropsModal site={site} />
|
<PropsModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/conversions">
|
<Route path="/conversions">
|
||||||
<ConversionsModal site={site} />
|
<ConversionsModal />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path={["/filter/:field"]}>
|
<Route path={["/filter/:field"]}>
|
||||||
<FilterModal site={site} />
|
<FilterModal site={site} />
|
||||||
</Route>
|
</Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</Route>
|
</Route>
|
||||||
</BrowserRouter >
|
</QueryContextProvider>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
59
assets/js/dashboard/site-context.js
Normal file
59
assets/js/dashboard/site-context.js
Normal 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;
|
@ -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" />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(() => {
|
||||||
|
@ -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')}
|
||||||
|
@ -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)
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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')) {
|
||||||
|
const [[_operation, _filterKey, clauses]] = getFiltersByKeyPrefix(query, "source")
|
||||||
|
return <ReferrerList query={query} site={site} source={clauses[0]} />
|
||||||
} else {
|
} else {
|
||||||
return <SourceList {...props} />
|
return <SourceList query={query} site={site} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
14
assets/js/dashboard/user-context.js
Normal file
14
assets/js/dashboard/user-context.js
Normal 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>
|
||||||
|
};
|
@ -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
|
||||||
|
200
assets/package-lock.json
generated
200
assets/package-lock.json
generated
@ -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"
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user