mirror of
https://github.com/plausible/analytics.git
synced 2024-09-11 09:56:02 +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",
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "babel-eslint",
|
||||
|
@ -7,36 +7,16 @@ import ErrorBoundary from './dashboard/error-boundary'
|
||||
import * as api from './dashboard/api'
|
||||
import * as timer from './dashboard/util/realtime-update-timer'
|
||||
import { filtersBackwardsCompatibilityRedirect } from './dashboard/query';
|
||||
import SiteContextProvider, { parseSiteFromDataset } from './dashboard/site-context';
|
||||
import UserContextProvider from './dashboard/user-context'
|
||||
|
||||
timer.start()
|
||||
|
||||
const container = document.getElementById('stats-react-container')
|
||||
|
||||
if (container) {
|
||||
const site = {
|
||||
domain: container.dataset.domain,
|
||||
offset: container.dataset.offset,
|
||||
hasGoals: container.dataset.hasGoals === 'true',
|
||||
hasProps: container.dataset.hasProps === 'true',
|
||||
funnelsAvailable: container.dataset.funnelsAvailable === 'true',
|
||||
propsAvailable: container.dataset.propsAvailable === 'true',
|
||||
conversionsOptedOut: container.dataset.conversionsOptedOut === 'true',
|
||||
funnelsOptedOut: container.dataset.funnelsOptedOut === 'true',
|
||||
propsOptedOut: container.dataset.propsOptedOut === 'true',
|
||||
revenueGoals: JSON.parse(container.dataset.revenueGoals),
|
||||
funnels: JSON.parse(container.dataset.funnels),
|
||||
statsBegin: container.dataset.statsBegin,
|
||||
nativeStatsBegin: container.dataset.nativeStatsBegin,
|
||||
embedded: container.dataset.embedded,
|
||||
background: container.dataset.background,
|
||||
isDbip: container.dataset.isDbip === 'true',
|
||||
flags: JSON.parse(container.dataset.flags),
|
||||
validIntervalsByPeriod: JSON.parse(container.dataset.validIntervalsByPeriod),
|
||||
shared: !!container.dataset.sharedLinkAuth,
|
||||
}
|
||||
const site = parseSiteFromDataset(container.dataset)
|
||||
|
||||
const loggedIn = container.dataset.loggedIn === 'true'
|
||||
const currentUserRole = container.dataset.currentUserRole
|
||||
const sharedLinkAuth = container.dataset.sharedLinkAuth
|
||||
if (sharedLinkAuth) {
|
||||
api.setSharedLinkAuth(sharedLinkAuth)
|
||||
@ -46,7 +26,11 @@ if (container) {
|
||||
|
||||
const app = (
|
||||
<ErrorBoundary>
|
||||
<Router site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
|
||||
<SiteContextProvider site={site}>
|
||||
<UserContextProvider role={container.dataset.currentUserRole} loggedIn={container.dataset.loggedIn === 'true'}>
|
||||
<Router />
|
||||
</UserContextProvider>
|
||||
</SiteContextProvider>
|
||||
</ErrorBoundary>
|
||||
)
|
||||
|
||||
|
@ -7,6 +7,8 @@ import classNames from 'classnames'
|
||||
import * as storage from './util/storage'
|
||||
import Flatpickr from 'react-flatpickr'
|
||||
import { parseNaiveDate, formatISO, formatDateRange } from './util/date.js'
|
||||
import { useQueryContext } from './query-context.js'
|
||||
import { useSiteContext } from './site-context.js'
|
||||
|
||||
const COMPARISON_MODES = {
|
||||
'off': 'Disable comparison',
|
||||
@ -19,11 +21,11 @@ const DEFAULT_COMPARISON_MODE = 'previous_period'
|
||||
|
||||
export const COMPARISON_DISABLED_PERIODS = ['realtime', 'all']
|
||||
|
||||
export const getStoredMatchDayOfWeek = function(domain) {
|
||||
export const getStoredMatchDayOfWeek = function (domain) {
|
||||
return storage.getItem(`comparison_match_day_of_week__${domain}`) || 'true'
|
||||
}
|
||||
|
||||
export const getStoredComparisonMode = function(domain) {
|
||||
export const getStoredComparisonMode = function (domain) {
|
||||
const mode = storage.getItem(`comparison_mode__${domain}`)
|
||||
if (Object.keys(COMPARISON_MODES).includes(mode)) {
|
||||
return mode
|
||||
@ -32,16 +34,16 @@ export const getStoredComparisonMode = function(domain) {
|
||||
}
|
||||
}
|
||||
|
||||
const storeComparisonMode = function(domain, mode) {
|
||||
const storeComparisonMode = function (domain, mode) {
|
||||
if (mode == "custom") return
|
||||
storage.setItem(`comparison_mode__${domain}`, mode)
|
||||
}
|
||||
|
||||
export const isComparisonEnabled = function(mode) {
|
||||
export const isComparisonEnabled = function (mode) {
|
||||
return mode && mode !== "off"
|
||||
}
|
||||
|
||||
export const toggleComparisons = function(history, query, site) {
|
||||
export const toggleComparisons = function (history, query, site) {
|
||||
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return
|
||||
|
||||
if (isComparisonEnabled(query.comparison)) {
|
||||
@ -72,14 +74,14 @@ function ComparisonModeOption({ label, value, isCurrentlySelected, updateMode, s
|
||||
"font-bold": isCurrentlySelected,
|
||||
})
|
||||
|
||||
return <button className={buttonClass}>{ label }</button>
|
||||
return <button className={buttonClass}>{label}</button>
|
||||
}
|
||||
|
||||
const disabled = isCurrentlySelected && value !== "custom"
|
||||
|
||||
return (
|
||||
<Menu.Item key={value} onClick={click} disabled={disabled}>
|
||||
{ render }
|
||||
{render}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
@ -112,7 +114,9 @@ function MatchDayOfWeekInput({ history, query, site }) {
|
||||
</>
|
||||
}
|
||||
|
||||
const ComparisonInput = function({ site, query, history }) {
|
||||
const ComparisonInput = function ({ history }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
if (COMPARISON_DISABLED_PERIODS.includes(query.period)) return null
|
||||
if (!isComparisonEnabled(query.comparison)) return null
|
||||
|
||||
@ -129,9 +133,12 @@ const ComparisonInput = function({ site, query, history }) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const calendar = React.useRef(null)
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const [uiMode, setUiMode] = React.useState("menu")
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
React.useEffect(() => {
|
||||
if (uiMode == "datepicker") {
|
||||
setTimeout(() => calendar.current.flatpickr.open(), 100)
|
||||
@ -162,7 +169,7 @@ const ComparisonInput = function({ site, query, history }) {
|
||||
<div className="min-w-32 md:w-48 md:relative">
|
||||
<Menu as="div" className="relative inline-block pl-2 w-full">
|
||||
<Menu.Button className="bg-white text-gray-800 text-xs md:text-sm font-medium dark:bg-gray-800 dark:hover:bg-gray-900 dark:text-gray-200 hover:bg-gray-200 flex md:px-3 px-2 py-2 items-center justify-between leading-tight rounded shadow cursor-pointer w-full truncate">
|
||||
<span className="truncate">{ buildLabel(site, query) }</span>
|
||||
<span className="truncate">{buildLabel(site, query)}</span>
|
||||
<ChevronDownIcon className="hidden sm:inline-block h-4 w-4 md:h-5 md:w-5 text-gray-500 ml-2" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
<Transition
|
||||
@ -174,18 +181,18 @@ const ComparisonInput = function({ site, query, history }) {
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95">
|
||||
<Menu.Items className="py-1 text-left origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-800 ring-1 ring-black ring-opacity-5 focus:outline-none z-10" static>
|
||||
{ Object.keys(COMPARISON_MODES).map((key) => ComparisonModeOption({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode, setUiMode })) }
|
||||
{ query.comparison !== "custom" && <span>
|
||||
{Object.keys(COMPARISON_MODES).map((key) => ComparisonModeOption({ label: COMPARISON_MODES[key], value: key, isCurrentlySelected: key == query.comparison, updateMode, setUiMode }))}
|
||||
{query.comparison !== "custom" && <span>
|
||||
<hr className="my-1" />
|
||||
<MatchDayOfWeekInput query={query} history={history} site={site} />
|
||||
</span>}
|
||||
</Menu.Items>
|
||||
</Transition>
|
||||
|
||||
{ uiMode == "datepicker" &&
|
||||
{uiMode == "datepicker" &&
|
||||
<div className="h-0 md:absolute">
|
||||
<Flatpickr ref={calendar} options={flatpickrOptions} className="invisible" />
|
||||
</div> }
|
||||
</div>}
|
||||
</Menu>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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 { COMPARISON_DISABLED_PERIODS, toggleComparisons, isComparisonEnabled } from "../dashboard/comparison-input.js"
|
||||
import classNames from "classnames"
|
||||
import { useQueryContext } from "./query-context.js";
|
||||
import { useSiteContext } from "./site-context.js";
|
||||
|
||||
function renderArrow(query, site, period, prevDate, nextDate) {
|
||||
const insertionDate = parseUTCDate(site.statsBegin);
|
||||
@ -156,7 +158,9 @@ function DisplayPeriod({ query, site }) {
|
||||
return 'Realtime'
|
||||
}
|
||||
|
||||
function DatePicker({ query, site, history }) {
|
||||
function DatePicker({ history }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
const [open, setOpen] = useState(false)
|
||||
const [mode, setMode] = useState('menu')
|
||||
const dropDownNode = useRef(null)
|
||||
|
@ -16,6 +16,8 @@ import {
|
||||
getLabel,
|
||||
FILTER_OPERATIONS_DISPLAY_NAMES
|
||||
} from "./util/filters"
|
||||
import { useQueryContext } from './query-context';
|
||||
import { useSiteContext } from './site-context';
|
||||
|
||||
const WRAPSTATE = { unwrapped: 0, waiting: 1, wrapped: 2 }
|
||||
|
||||
@ -122,8 +124,10 @@ function DropdownContent({ history, site, query, wrapped }) {
|
||||
)
|
||||
}
|
||||
|
||||
function Filters(props) {
|
||||
const { history, query, site } = props
|
||||
function Filters({ history }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const [wrapped, setWrapped] = useState(WRAPSTATE.waiting)
|
||||
const [viewport, setViewport] = useState(1080)
|
||||
|
||||
|
@ -13,45 +13,51 @@ import Behaviours from './stats/behaviours'
|
||||
import ComparisonInput from './comparison-input'
|
||||
import { withPinnedHeader } from './pinned-header-hoc';
|
||||
import { statsBoxClass } from './index';
|
||||
import { useSiteContext } from './site-context';
|
||||
import { useQueryContext } from './query-context';
|
||||
import { useUserContext } from './user-context';
|
||||
|
||||
function Historical(props) {
|
||||
function Historical({ stuck, importedDataInView, updateImportedDataInView }) {
|
||||
const site = useSiteContext();
|
||||
const user = useUserContext();
|
||||
const { query } = useQueryContext();
|
||||
const tooltipBoundary = React.useRef(null)
|
||||
|
||||
return (
|
||||
<div className="mb-12">
|
||||
<div id="stats-container-top"></div>
|
||||
<div className={`relative top-0 sm:py-3 py-2 z-10 ${props.stuck && !props.site.embedded ? 'sticky fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}>
|
||||
<div className={`relative top-0 sm:py-3 py-2 z-10 ${stuck && !site.embedded ? 'sticky fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}>
|
||||
<div className="items-center w-full flex">
|
||||
<div className="flex items-center w-full" ref={tooltipBoundary}>
|
||||
<SiteSwitcher site={props.site} loggedIn={props.loggedIn} currentUserRole={props.currentUserRole} />
|
||||
<CurrentVisitors site={props.site} query={props.query} lastLoadTimestamp={props.lastLoadTimestamp} tooltipBoundary={tooltipBoundary.current} />
|
||||
<Filters className="flex" site={props.site} query={props.query} history={props.history} />
|
||||
<SiteSwitcher site={site} loggedIn={user.loggedIn} currentUserRole={user.role} />
|
||||
<CurrentVisitors tooltipBoundary={tooltipBoundary.current} />
|
||||
<Filters className="flex" />
|
||||
</div>
|
||||
<Datepicker site={props.site} query={props.query} />
|
||||
<ComparisonInput site={props.site} query={props.query} />
|
||||
<Datepicker />
|
||||
<ComparisonInput />
|
||||
</div>
|
||||
</div>
|
||||
<VisitorGraph site={props.site} query={props.query} updateImportedDataInView={props.updateImportedDataInView}/>
|
||||
<VisitorGraph updateImportedDataInView={updateImportedDataInView} />
|
||||
|
||||
<div className="w-full md:flex">
|
||||
<div className={ statsBoxClass }>
|
||||
<Sources site={props.site} query={props.query} />
|
||||
<div className={statsBoxClass}>
|
||||
<Sources />
|
||||
</div>
|
||||
<div className={ statsBoxClass }>
|
||||
<Pages site={props.site} query={props.query} />
|
||||
<div className={statsBoxClass}>
|
||||
<Pages />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full md:flex">
|
||||
<div className={ statsBoxClass }>
|
||||
<Locations site={props.site} query={props.query} />
|
||||
<div className={statsBoxClass}>
|
||||
<Locations site={site} query={query} />
|
||||
</div>
|
||||
<div className={ statsBoxClass }>
|
||||
<Devices site={props.site} query={props.query} />
|
||||
<div className={statsBoxClass}>
|
||||
<Devices />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Behaviours site={props.site} query={props.query} currentUserRole={props.currentUserRole} importedDataInView={props.importedDataInView}/>
|
||||
<Behaviours importedDataInView={importedDataInView} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,46 +1,26 @@
|
||||
import React from 'react'
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import Historical from './historical'
|
||||
import Realtime from './realtime'
|
||||
import withQueryContext from './components/query-context-hoc';
|
||||
import Realtime, { useIsRealtimeDashboard } from './realtime'
|
||||
|
||||
export const statsBoxClass = "stats-item relative w-full mt-6 p-4 flex flex-col bg-white dark:bg-gray-825 shadow-xl rounded"
|
||||
|
||||
function Dashboard(props) {
|
||||
const {
|
||||
site,
|
||||
loggedIn,
|
||||
currentUserRole,
|
||||
query,
|
||||
importedDataInView,
|
||||
updateImportedDataInView,
|
||||
lastLoadTimestamp
|
||||
} = props
|
||||
export function Dashboard() {
|
||||
const isRealTimeDashboard = useIsRealtimeDashboard();
|
||||
const [importedDataInView, setImportedDataInView] = useState(false)
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
if (isRealTimeDashboard) {
|
||||
return (
|
||||
<Realtime
|
||||
site={site}
|
||||
loggedIn={loggedIn}
|
||||
currentUserRole={currentUserRole}
|
||||
query={query}
|
||||
lastLoadTimestamp={lastLoadTimestamp}
|
||||
/>
|
||||
<Realtime />
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Historical
|
||||
site={site}
|
||||
loggedIn={loggedIn}
|
||||
currentUserRole={currentUserRole}
|
||||
query={query}
|
||||
lastLoadTimestamp={lastLoadTimestamp}
|
||||
importedDataInView={importedDataInView}
|
||||
updateImportedDataInView={updateImportedDataInView}
|
||||
updateImportedDataInView={setImportedDataInView}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(Dashboard))
|
||||
export default Dashboard
|
||||
|
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 SiteSwitcher from './site-switcher'
|
||||
@ -11,9 +11,20 @@ import Devices from './stats/devices'
|
||||
import Behaviours from './stats/behaviours'
|
||||
import { withPinnedHeader } from './pinned-header-hoc';
|
||||
import { statsBoxClass } from './index';
|
||||
import { useSiteContext } from './site-context';
|
||||
import { useUserContext } from './user-context';
|
||||
import { useQueryContext } from './query-context';
|
||||
import { isRealTimeDashboard } from './util/filters';
|
||||
|
||||
function Realtime(props) {
|
||||
const {site, query, history, stuck, loggedIn, currentUserRole, lastLoadTimestamp} = props
|
||||
export const useIsRealtimeDashboard = () => {
|
||||
const { query: { period } } = useQueryContext();
|
||||
return useMemo(() => isRealTimeDashboard({ period }), [period]);
|
||||
}
|
||||
|
||||
function Realtime({ stuck }) {
|
||||
const site = useSiteContext();
|
||||
const user = useUserContext();
|
||||
const { query } = useQueryContext();
|
||||
const navClass = site.embedded ? 'relative' : 'sticky'
|
||||
|
||||
return (
|
||||
@ -22,30 +33,30 @@ function Realtime(props) {
|
||||
<div className={`${navClass} top-0 sm:py-3 py-2 z-10 ${stuck && !site.embedded ? 'fullwidth-shadow bg-gray-50 dark:bg-gray-850' : ''}`}>
|
||||
<div className="items-center w-full flex">
|
||||
<div className="flex items-center w-full">
|
||||
<SiteSwitcher site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
|
||||
<Filters className="flex" site={site} query={query} history={history} />
|
||||
<SiteSwitcher site={site} loggedIn={user.loggedIn} currentUserRole={user.role} />
|
||||
<Filters className="flex" />
|
||||
</div>
|
||||
<Datepicker site={site} query={query} />
|
||||
<Datepicker />
|
||||
</div>
|
||||
</div>
|
||||
<VisitorGraph site={site} query={query} lastLoadTimestamp={lastLoadTimestamp}/>
|
||||
<VisitorGraph />
|
||||
<div className="w-full md:flex">
|
||||
<div className={ statsBoxClass }>
|
||||
<Sources site={site} query={query} />
|
||||
<div className={statsBoxClass}>
|
||||
<Sources />
|
||||
</div>
|
||||
<div className={ statsBoxClass }>
|
||||
<Pages site={site} query={query} />
|
||||
<div className={statsBoxClass}>
|
||||
<Pages />
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full md:flex">
|
||||
<div className={ statsBoxClass }>
|
||||
<div className={statsBoxClass}>
|
||||
<Locations site={site} query={query} />
|
||||
</div>
|
||||
<div className={ statsBoxClass }>
|
||||
<Devices site={site} query={query} />
|
||||
<div className={statsBoxClass}>
|
||||
<Devices />
|
||||
</div>
|
||||
</div>
|
||||
<Behaviours site={site} query={query} currentUserRole={currentUserRole} />
|
||||
<Behaviours />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { BrowserRouter, Switch, Route, useLocation } from "react-router-dom";
|
||||
|
||||
import Dash from './index'
|
||||
import Dashboard from './index'
|
||||
import SourcesModal from './stats/modals/sources'
|
||||
import ReferrersDrilldownModal from './stats/modals/referrer-drilldown'
|
||||
import GoogleKeywordsModal from './stats/modals/google-keywords'
|
||||
@ -12,6 +12,8 @@ import LocationsModal from './stats/modals/locations-modal';
|
||||
import PropsModal from './stats/modals/props'
|
||||
import ConversionsModal from './stats/modals/conversions'
|
||||
import FilterModal from './stats/modals/filter-modal'
|
||||
import QueryContextProvider from './query-context';
|
||||
import { useSiteContext } from './site-context';
|
||||
|
||||
function ScrollToTop() {
|
||||
const location = useLocation();
|
||||
@ -25,45 +27,48 @@ function ScrollToTop() {
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function Router({ site, loggedIn, currentUserRole }) {
|
||||
export default function Router() {
|
||||
const site = useSiteContext()
|
||||
return (
|
||||
<BrowserRouter basename={site.shared ? `/share/${encodeURIComponent(site.domain)}` : encodeURIComponent(site.domain)}>
|
||||
<QueryContextProvider>
|
||||
<Route path="/">
|
||||
<ScrollToTop />
|
||||
<Dash site={site} loggedIn={loggedIn} currentUserRole={currentUserRole} />
|
||||
<Dashboard />
|
||||
<Switch>
|
||||
<Route exact path={["/sources", "/utm_mediums", "/utm_sources", "/utm_campaigns", "/utm_contents", "/utm_terms"]}>
|
||||
<SourcesModal site={site} />
|
||||
<SourcesModal />
|
||||
</Route>
|
||||
<Route exact path="/referrers/Google">
|
||||
<GoogleKeywordsModal site={site} />
|
||||
</Route>
|
||||
<Route exact path="/referrers/:referrer">
|
||||
<ReferrersDrilldownModal site={site} />
|
||||
<ReferrersDrilldownModal />
|
||||
</Route>
|
||||
<Route path="/pages">
|
||||
<PagesModal site={site} />
|
||||
<PagesModal />
|
||||
</Route>
|
||||
<Route path="/entry-pages">
|
||||
<EntryPagesModal site={site} />
|
||||
<EntryPagesModal />
|
||||
</Route>
|
||||
<Route path="/exit-pages">
|
||||
<ExitPagesModal site={site} />
|
||||
<ExitPagesModal />
|
||||
</Route>
|
||||
<Route exact path={["/countries", "/regions", "/cities"]}>
|
||||
<LocationsModal site={site} />
|
||||
<LocationsModal />
|
||||
</Route>
|
||||
<Route path="/custom-prop-values/:prop_key">
|
||||
<PropsModal site={site} />
|
||||
<PropsModal />
|
||||
</Route>
|
||||
<Route path="/conversions">
|
||||
<ConversionsModal site={site} />
|
||||
<ConversionsModal />
|
||||
</Route>
|
||||
<Route path={["/filter/:field"]}>
|
||||
<FilterModal site={site} />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Route>
|
||||
</BrowserRouter >
|
||||
</QueryContextProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
}
|
||||
|
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 { SPECIAL_GOALS } from './goal-conversions'
|
||||
import { hasGoalFilter } from '../../util/filters'
|
||||
import { useSiteContext } from '../../site-context'
|
||||
import { useQueryContext } from '../../query-context'
|
||||
import { useUserContext } from '../../user-context'
|
||||
|
||||
/*global BUILD_EXTRA*/
|
||||
/*global require*/
|
||||
@ -35,9 +38,12 @@ export const sectionTitles = {
|
||||
[FUNNELS]: 'Funnels'
|
||||
}
|
||||
|
||||
export default function Behaviours(props) {
|
||||
const { site, query, currentUserRole } = props
|
||||
const adminAccess = ['owner', 'admin', 'super_admin'].includes(currentUserRole)
|
||||
export default function Behaviours({ importedDataInView }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
const user = useUserContext();
|
||||
|
||||
const adminAccess = ['owner', 'admin', 'super_admin'].includes(user.role)
|
||||
const tabKey = `behavioursTab__${site.domain}`
|
||||
const funnelKey = `behavioursTabFunnel__${site.domain}`
|
||||
const [enabledModes, setEnabledModes] = useState(getEnabledModes())
|
||||
@ -309,7 +315,7 @@ export default function Behaviours(props) {
|
||||
// If the feature is not supported by the site owner's subscription,
|
||||
// it only makes sense to display the feature tab to the owner itself
|
||||
// as only they can upgrade to make the feature available.
|
||||
const callToActionIsMissing = !isAvailable && currentUserRole !== 'owner'
|
||||
const callToActionIsMissing = !isAvailable && user.role !== 'owner'
|
||||
|
||||
if (!isOptedOut && !callToActionIsMissing) {
|
||||
enabledModes.push(feature)
|
||||
@ -341,7 +347,7 @@ export default function Behaviours(props) {
|
||||
} else if (mode === PROPS) {
|
||||
return <ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason} message="Imported data is unavailable in this view" />
|
||||
} else {
|
||||
return <ImportedQueryUnsupportedWarning altCondition={props.importedDataInView} message="Imported data is unavailable in this view" />
|
||||
return <ImportedQueryUnsupportedWarning altCondition={importedDataInView} message="Imported data is unavailable in this view" />
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,12 @@ import * as api from '../api'
|
||||
import * as url from '../util/url'
|
||||
import { Tooltip } from '../util/tooltip';
|
||||
import { SecondsSinceLastLoad } from '../util/seconds-since-last-load';
|
||||
import { useQueryContext } from '../query-context';
|
||||
import { useSiteContext } from '../site-context';
|
||||
|
||||
export default function CurrentVisitors(props) {
|
||||
const { site, query, lastLoadTimestamp, tooltipBoundary } = props
|
||||
export default function CurrentVisitors({ tooltipBoundary }) {
|
||||
const { query, lastLoadTimestamp } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
const [currentVisitors, setCurrentVisitors] = useState(null)
|
||||
|
||||
const updateCount = useCallback(() => {
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import * as storage from '../../util/storage'
|
||||
import { getFiltersByKeyPrefix, hasGoalFilter, isFilteringOnFixedValue } from '../../util/filters'
|
||||
import ListReport from '../reports/list'
|
||||
@ -6,6 +6,8 @@ import * as metrics from '../reports/metrics'
|
||||
import * as api from '../../api'
|
||||
import * as url from '../../util/url'
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
|
||||
import { useQueryContext } from '../../query-context';
|
||||
import { useSiteContext } from '../../site-context';
|
||||
|
||||
// Icons copied from https://github.com/alrra/browser-logos
|
||||
const BROWSER_ICONS = {
|
||||
@ -57,7 +59,7 @@ function Browsers({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ meta: {plot: true}}),
|
||||
metrics.createVisitors({ meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
!hasGoalFilter(query) && metrics.createPercentage()
|
||||
].filter(metric => !!metric)
|
||||
@ -80,9 +82,11 @@ function BrowserVersions({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/browser-versions'), query)
|
||||
.then(res => {
|
||||
return {...res, results: res.results.map((row => {
|
||||
return {...row, name: `${row.browser} ${row.name}`, version: row.name}
|
||||
}))}
|
||||
return {
|
||||
...res, results: res.results.map((row => {
|
||||
return { ...row, name: `${row.browser} ${row.name}`, version: row.name }
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -102,7 +106,7 @@ function BrowserVersions({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ meta: {plot: true}}),
|
||||
metrics.createVisitors({ meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
!hasGoalFilter(query) && metrics.createPercentage()
|
||||
].filter(metric => !!metric)
|
||||
@ -166,9 +170,9 @@ function OperatingSystems({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ meta: {plot: true}}),
|
||||
metrics.createVisitors({ meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
!hasGoalFilter(query) && metrics.createPercentage({meta: {hiddenonMobile: true}})
|
||||
!hasGoalFilter(query) && metrics.createPercentage({ meta: { hiddenonMobile: true } })
|
||||
].filter(metric => !!metric)
|
||||
}
|
||||
|
||||
@ -193,9 +197,11 @@ function OperatingSystemVersions({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/operating-system-versions'), query)
|
||||
.then(res => {
|
||||
return {...res, results: res.results.map((row => {
|
||||
return {...row, name: `${row.os} ${row.name}`, version: row.name}
|
||||
}))}
|
||||
return {
|
||||
...res, results: res.results.map((row => {
|
||||
return { ...row, name: `${row.os} ${row.name}`, version: row.name }
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -215,7 +221,7 @@ function OperatingSystemVersions({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ meta: {plot: true}}),
|
||||
metrics.createVisitors({ meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
!hasGoalFilter(query) && metrics.createPercentage()
|
||||
].filter(metric => !!metric)
|
||||
@ -255,7 +261,7 @@ function ScreenSizes({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ meta: {plot: true}}),
|
||||
metrics.createVisitors({ meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
!hasGoalFilter(query) && metrics.createPercentage()
|
||||
].filter(metric => !!metric)
|
||||
@ -296,8 +302,10 @@ function iconFor(screenSize) {
|
||||
}
|
||||
}
|
||||
|
||||
export default function Devices(props) {
|
||||
const {site, query} = props
|
||||
export default function Devices() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const tabKey = `deviceTab__${site.domain}`
|
||||
const storedTab = storage.getItem(tabKey)
|
||||
const [mode, setMode] = useState(storedTab || 'browser')
|
||||
@ -362,7 +370,7 @@ export default function Devices(props) {
|
||||
<div className="flex justify-between w-full">
|
||||
<div className="flex gap-x-1">
|
||||
<h3 className="font-bold dark:text-gray-100">Devices</h3>
|
||||
<ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason}/>
|
||||
<ImportedQueryUnsupportedWarning loading={loading} query={query} skipImportedReason={skipImportedReason} />
|
||||
</div>
|
||||
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
||||
{renderPill('Browser', 'browser')}
|
||||
|
@ -64,10 +64,12 @@ function storeInterval(period, domain, interval) {
|
||||
}
|
||||
|
||||
function subscribeKeybinding(element) {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const handleKeyPress = useCallback((event) => {
|
||||
if (isKeyPressed(event, "i")) element.current?.click()
|
||||
}, [])
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
useEffect(() => {
|
||||
document.addEventListener('keydown', handleKeyPress)
|
||||
return () => document.removeEventListener('keydown', handleKeyPress)
|
||||
@ -90,6 +92,7 @@ export const getCurrentInterval = function(site, query) {
|
||||
export function IntervalPicker({ query, site, onIntervalUpdate }) {
|
||||
if (query.period == 'realtime') return null
|
||||
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const menuElement = React.useRef(null)
|
||||
const options = validIntervals(site, query)
|
||||
const currentInterval = getCurrentInterval(site, query)
|
||||
|
@ -11,6 +11,8 @@ import FadeIn from '../../fade-in';
|
||||
import * as url from '../../util/url'
|
||||
import { isComparisonEnabled } from '../../comparison-input'
|
||||
import LineGraphWithRouter from './line-graph'
|
||||
import { useQueryContext } from '../../query-context';
|
||||
import { useSiteContext } from '../../site-context';
|
||||
|
||||
function fetchTopStats(site, query) {
|
||||
const q = { ...query }
|
||||
@ -23,12 +25,14 @@ function fetchTopStats(site, query) {
|
||||
}
|
||||
|
||||
function fetchMainGraph(site, query, metric, interval) {
|
||||
const params = {metric, interval}
|
||||
const params = { metric, interval }
|
||||
return api.get(url.apiPath(site, '/main-graph'), query, params)
|
||||
}
|
||||
|
||||
export default function VisitorGraph(props) {
|
||||
const {site, query, lastLoadTimestamp} = props
|
||||
export default function VisitorGraph({ updateImportedDataInView }) {
|
||||
const { query, lastLoadTimestamp } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const isRealtime = query.period === 'realtime'
|
||||
const isDarkTheme = document.querySelector('html').classList.contains('dark') || false
|
||||
|
||||
@ -81,8 +85,8 @@ export default function VisitorGraph(props) {
|
||||
function fetchTopStatsAndGraphData() {
|
||||
fetchTopStats(site, query)
|
||||
.then((res) => {
|
||||
if (props.updateImportedDataInView) {
|
||||
props.updateImportedDataInView(res.includes_imported)
|
||||
if (updateImportedDataInView) {
|
||||
updateImportedDataInView(res.includes_imported)
|
||||
}
|
||||
setTopStatData(res)
|
||||
setTopStatsLoading(false)
|
||||
@ -149,7 +153,7 @@ export default function VisitorGraph(props) {
|
||||
{graphRefreshing && renderLoader()}
|
||||
<div className="absolute right-4 -top-8 py-1 flex items-center">
|
||||
{!isRealtime && <StatsExport site={site} query={query} />}
|
||||
<SamplingNotice samplePercent={topStatData}/>
|
||||
<SamplingNotice samplePercent={topStatData} />
|
||||
<WithImportedSwitch query={query} info={topStatData && topStatData.with_imported_switch} />
|
||||
<IntervalPicker site={site} query={query} onIntervalUpdate={onIntervalUpdate} />
|
||||
</div>
|
||||
|
@ -4,6 +4,8 @@ import * as api from '../../api'
|
||||
import { useDebouncedEffect, useMountedEffect } from '../../custom-hooks'
|
||||
import { trimURL } from '../../util/url'
|
||||
import { FilterLink } from "../reports/list";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
const LIMIT = 100
|
||||
const MIN_HEIGHT_PX = 500
|
||||
@ -33,11 +35,6 @@ const MIN_HEIGHT_PX = 500
|
||||
|
||||
// ### Required Props
|
||||
|
||||
// * `site` - the current dashboard site
|
||||
|
||||
// * `query` - a read-only query object representing the query state of the
|
||||
// dashboard (e.g. `filters`, `period`, `with_imported`, etc)
|
||||
|
||||
// * `reportInfo` - a map with the following required keys:
|
||||
|
||||
// * `title` - the title of the report to render on the top left
|
||||
@ -77,8 +74,6 @@ const MIN_HEIGHT_PX = 500
|
||||
// * `afterFetchNextPage` - a function with the same behaviour as `afterFetchData`,
|
||||
// but will be called after a successful next page load in `fetchNextPage`.
|
||||
export default function BreakdownModal({
|
||||
site,
|
||||
query,
|
||||
reportInfo,
|
||||
metrics,
|
||||
renderIcon,
|
||||
@ -89,6 +84,9 @@ export default function BreakdownModal({
|
||||
addSearchFilter,
|
||||
getFilterInfo
|
||||
}) {
|
||||
const {query} = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const endpoint = `/api/stats/${encodeURIComponent(site.domain)}${reportInfo.endpoint}`
|
||||
|
||||
const [initialLoading, setInitialLoading] = useState(true)
|
||||
|
@ -1,15 +1,11 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
import Modal from './modal'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
|
||||
|
||||
/*global BUILD_EXTRA*/
|
||||
function ConversionsModal(props) {
|
||||
const { site, query } = props
|
||||
function ConversionsModal() {
|
||||
const [showRevenue, setShowRevenue] = useState(false)
|
||||
|
||||
const reportInfo = {
|
||||
@ -28,8 +24,8 @@ function ConversionsModal(props) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => "Uniques"}),
|
||||
metrics.createEvents({renderLabel: (_query) => "Total"}),
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Uniques" }),
|
||||
metrics.createEvents({ renderLabel: (_query) => "Total" }),
|
||||
metrics.createConversionRate(),
|
||||
showRevenue && metrics.createAverageRevenue(),
|
||||
showRevenue && metrics.createTotalRevenue(),
|
||||
@ -54,10 +50,8 @@ function ConversionsModal(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
afterFetchData={BUILD_EXTRA ? afterFetchData : undefined}
|
||||
@ -69,4 +63,4 @@ function ConversionsModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(ConversionsModal))
|
||||
export default ConversionsModal
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React, {useCallback} from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import React, { useCallback } from "react";
|
||||
import Modal from './modal'
|
||||
import { hasGoalFilter } from "../../util/filters";
|
||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||
import { addFilter } from '../../query'
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from '../reports/metrics'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
|
||||
function EntryPagesModal(props) {
|
||||
const { site, query } = props
|
||||
function EntryPagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Entry Pages',
|
||||
@ -32,29 +31,27 @@ function EntryPagesModal(props) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisits({renderLabel: (_query) => "Total Entrances" }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisits({ renderLabel: (_query) => "Total Entrances" }),
|
||||
metrics.createVisitDuration()
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
getFilterInfo={getFilterInfo}
|
||||
@ -64,4 +61,4 @@ function EntryPagesModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(EntryPagesModal))
|
||||
export default EntryPagesModal
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React, {useCallback} from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import React, { useCallback } from "react";
|
||||
import Modal from './modal'
|
||||
import { hasGoalFilter } from "../../util/filters";
|
||||
import { addFilter } from '../../query'
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from '../reports/metrics'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
|
||||
function ExitPagesModal(props) {
|
||||
const { site, query } = props
|
||||
function ExitPagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Exit Pages',
|
||||
@ -32,29 +31,27 @@ function ExitPagesModal(props) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisits({renderLabel: (_query) => "Total Exits" }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisits({ renderLabel: (_query) => "Total Exits" }),
|
||||
metrics.createExitRate()
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
getFilterInfo={getFilterInfo}
|
||||
@ -64,4 +61,4 @@ function ExitPagesModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(ExitPagesModal))
|
||||
export default ExitPagesModal
|
||||
|
@ -2,19 +2,19 @@ import React, { useCallback } from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
import Modal from './modal'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import { hasGoalFilter } from "../../util/filters";
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
|
||||
const VIEWS = {
|
||||
countries: {title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country'},
|
||||
regions: {title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region'},
|
||||
cities: {title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City'},
|
||||
countries: { title: 'Top Countries', dimension: 'country', endpoint: '/countries', dimensionLabel: 'Country' },
|
||||
regions: { title: 'Top Regions', dimension: 'region', endpoint: '/regions', dimensionLabel: 'Region' },
|
||||
cities: { title: 'Top Cities', dimension: 'city', endpoint: '/cities', dimensionLabel: 'City' },
|
||||
}
|
||||
|
||||
function LocationsModal(props) {
|
||||
const { site, query, location } = props
|
||||
function LocationsModal({ location }) {
|
||||
const { query } = useQueryContext();
|
||||
|
||||
const urlParts = location.pathname.split('/')
|
||||
const currentView = urlParts[urlParts.length - 1]
|
||||
@ -32,19 +32,19 @@ function LocationsModal(props) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
currentView === 'countries' && metrics.createPercentage()
|
||||
].filter(metric => !!metric)
|
||||
}
|
||||
@ -56,10 +56,8 @@ function LocationsModal(props) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
getFilterInfo={getFilterInfo}
|
||||
@ -70,4 +68,4 @@ function LocationsModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(LocationsModal))
|
||||
export default withRouter(LocationsModal)
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React, {useCallback} from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
import Modal from './modal'
|
||||
import { hasGoalFilter } from "../../util/filters";
|
||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||
import { addFilter } from '../../query'
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from '../reports/metrics'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
|
||||
function PagesModal(props) {
|
||||
const { site, query } = props
|
||||
function PagesModal() {
|
||||
const { query } = useQueryContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: 'Top Pages',
|
||||
@ -37,7 +36,7 @@ function PagesModal(props) {
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
|
||||
]
|
||||
@ -52,10 +51,8 @@ function PagesModal(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
getFilterInfo={getFilterInfo}
|
||||
@ -65,4 +62,4 @@ function PagesModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(PagesModal))
|
||||
export default PagesModal
|
||||
|
@ -2,16 +2,18 @@ import React, { useCallback } from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
import Modal from './modal'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import { addFilter } from '../../query'
|
||||
import { specialTitleWhenGoalFilter } from "../behaviours/goal-conversions";
|
||||
import { EVENT_PROPS_PREFIX, hasGoalFilter } from "../../util/filters"
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import { revenueAvailable } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
import { useSiteContext } from "../../site-context";
|
||||
|
||||
function PropsModal(props) {
|
||||
const {site, query, location} = props
|
||||
function PropsModal({ location }) {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
const propKey = location.pathname.split('/').filter(i => i).pop()
|
||||
|
||||
/*global BUILD_EXTRA*/
|
||||
@ -37,8 +39,8 @@ function PropsModal(props) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => "Visitors"}),
|
||||
metrics.createEvents({renderLabel: (_query) => "Events"}),
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createEvents({ renderLabel: (_query) => "Events" }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
!hasGoalFilter(query) && metrics.createPercentage(),
|
||||
showRevenueMetrics && metrics.createAverageRevenue(),
|
||||
@ -47,10 +49,8 @@ function PropsModal(props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
getFilterInfo={getFilterInfo}
|
||||
@ -60,4 +60,4 @@ function PropsModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(PropsModal))
|
||||
export default withRouter(PropsModal)
|
||||
|
@ -2,14 +2,14 @@ import React, { useCallback } from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
import Modal from './modal'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import { hasGoalFilter } from "../../util/filters";
|
||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import { addFilter } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
|
||||
function ReferrerDrilldownModal(props) {
|
||||
const { site, query, match } = props
|
||||
function ReferrerDrilldownModal({ match }) {
|
||||
const { query } = useQueryContext();
|
||||
|
||||
const reportInfo = {
|
||||
title: "Referrer Drilldown",
|
||||
@ -33,19 +33,19 @@ function ReferrerDrilldownModal(props) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createBounceRate(),
|
||||
metrics.createVisitDuration()
|
||||
]
|
||||
@ -67,10 +67,8 @@ function ReferrerDrilldownModal(props) {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
getFilterInfo={getFilterInfo}
|
||||
@ -82,4 +80,4 @@ function ReferrerDrilldownModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(ReferrerDrilldownModal))
|
||||
export default withRouter(ReferrerDrilldownModal)
|
||||
|
@ -2,15 +2,15 @@ import React, { useCallback } from "react";
|
||||
import { withRouter } from 'react-router-dom'
|
||||
|
||||
import Modal from './modal'
|
||||
import withQueryContext from "../../components/query-context-hoc";
|
||||
import { hasGoalFilter } from "../../util/filters";
|
||||
import { hasGoalFilter, isRealTimeDashboard } from "../../util/filters";
|
||||
import BreakdownModal from "./breakdown-modal";
|
||||
import * as metrics from "../reports/metrics";
|
||||
import { addFilter } from "../../query";
|
||||
import { useQueryContext } from "../../query-context";
|
||||
|
||||
const VIEWS = {
|
||||
sources: {
|
||||
info: {title: 'Top Sources', dimension: 'source', endpoint: '/sources', dimensionLabel: 'Source'},
|
||||
info: { title: 'Top Sources', dimension: 'source', endpoint: '/sources', dimensionLabel: 'Source' },
|
||||
renderIcon: (listItem) => {
|
||||
return (
|
||||
<img
|
||||
@ -21,24 +21,24 @@ const VIEWS = {
|
||||
}
|
||||
},
|
||||
utm_mediums: {
|
||||
info: {title: 'Top UTM Mediums', dimension: 'utm_medium', endpoint: '/utm_mediums', dimensionLabel: 'UTM Medium'}
|
||||
info: { title: 'Top UTM Mediums', dimension: 'utm_medium', endpoint: '/utm_mediums', dimensionLabel: 'UTM Medium' }
|
||||
},
|
||||
utm_sources: {
|
||||
info: {title: 'Top UTM Sources', dimension: 'utm_source', endpoint: '/utm_sources', dimensionLabel: 'UTM Source'}
|
||||
info: { title: 'Top UTM Sources', dimension: 'utm_source', endpoint: '/utm_sources', dimensionLabel: 'UTM Source' }
|
||||
},
|
||||
utm_campaigns: {
|
||||
info: {title: 'Top UTM Campaigns', dimension: 'utm_campaign', endpoint: '/utm_campaigns', dimensionLabel: 'UTM Campaign'}
|
||||
info: { title: 'Top UTM Campaigns', dimension: 'utm_campaign', endpoint: '/utm_campaigns', dimensionLabel: 'UTM Campaign' }
|
||||
},
|
||||
utm_contents: {
|
||||
info: {title: 'Top UTM Contents', dimension: 'utm_content', endpoint: '/utm_contents', dimensionLabel: 'UTM Content'}
|
||||
info: { title: 'Top UTM Contents', dimension: 'utm_content', endpoint: '/utm_contents', dimensionLabel: 'UTM Content' }
|
||||
},
|
||||
utm_terms: {
|
||||
info: {title: 'Top UTM Terms', dimension: 'utm_term', endpoint: '/utm_terms', dimensionLabel: 'UTM Term'}
|
||||
info: { title: 'Top UTM Terms', dimension: 'utm_term', endpoint: '/utm_terms', dimensionLabel: 'UTM Term' }
|
||||
},
|
||||
}
|
||||
|
||||
function SourcesModal(props) {
|
||||
const { site, query, location } = props
|
||||
function SourcesModal({ location }) {
|
||||
const { query } = useQueryContext();
|
||||
|
||||
const urlParts = location.pathname.split('/')
|
||||
const currentView = urlParts[urlParts.length - 1]
|
||||
@ -60,29 +60,27 @@ function SourcesModal(props) {
|
||||
if (hasGoalFilter(query)) {
|
||||
return [
|
||||
metrics.createTotalVisitors(),
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Conversions'}),
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Conversions' }),
|
||||
metrics.createConversionRate()
|
||||
]
|
||||
}
|
||||
|
||||
if (query.period === 'realtime') {
|
||||
if (isRealTimeDashboard(query)) {
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => 'Current visitors'})
|
||||
metrics.createVisitors({ renderLabel: (_query) => 'Current visitors' })
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
metrics.createVisitors({renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createVisitors({ renderLabel: (_query) => "Visitors" }),
|
||||
metrics.createBounceRate(),
|
||||
metrics.createVisitDuration()
|
||||
]
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal site={site}>
|
||||
<Modal>
|
||||
<BreakdownModal
|
||||
site={site}
|
||||
query={query}
|
||||
reportInfo={reportInfo}
|
||||
metrics={chooseMetrics()}
|
||||
getFilterInfo={getFilterInfo}
|
||||
@ -93,4 +91,4 @@ function SourcesModal(props) {
|
||||
)
|
||||
}
|
||||
|
||||
export default withRouter(withQueryContext(SourcesModal))
|
||||
export default withRouter(SourcesModal)
|
||||
|
@ -7,6 +7,8 @@ import ListReport from './../reports/list'
|
||||
import * as metrics from './../reports/metrics'
|
||||
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning';
|
||||
import { hasGoalFilter } from '../../util/filters';
|
||||
import { useQueryContext } from '../../query-context';
|
||||
import { useSiteContext } from '../../site-context';
|
||||
|
||||
function EntryPages({ query, site, afterFetchData }) {
|
||||
function fetchData() {
|
||||
@ -26,7 +28,7 @@ function EntryPages({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({defaultLabel: 'Unique Entrances', meta: {plot: true}}),
|
||||
metrics.createVisitors({ defaultLabel: 'Unique Entrances', meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
].filter(metric => !!metric)
|
||||
}
|
||||
@ -64,7 +66,7 @@ function ExitPages({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({defaultLabel: 'Unique Exits', meta: {plot: true}}),
|
||||
metrics.createVisitors({ defaultLabel: 'Unique Exits', meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
].filter(metric => !!metric)
|
||||
}
|
||||
@ -102,7 +104,7 @@ function TopPages({ query, site, afterFetchData }) {
|
||||
|
||||
function chooseMetrics() {
|
||||
return [
|
||||
metrics.createVisitors({ meta: {plot: true}}),
|
||||
metrics.createVisitors({ meta: { plot: true } }),
|
||||
hasGoalFilter(query) && metrics.createConversionRate(),
|
||||
].filter(metric => !!metric)
|
||||
}
|
||||
@ -128,8 +130,10 @@ const labelFor = {
|
||||
'exit-pages': 'Exit Pages',
|
||||
}
|
||||
|
||||
export default function Pages(props) {
|
||||
const { site, query } = props
|
||||
export default function Pages() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
|
||||
const tabKey = `pageTab__${site.domain}`
|
||||
const storedTab = storage.getItem(tabKey)
|
||||
const [mode, setMode] = useState(storedTab || 'pages')
|
||||
|
@ -3,15 +3,19 @@ import SearchTerms from './search-terms'
|
||||
import SourceList from './source-list'
|
||||
import ReferrerList from './referrer-list'
|
||||
import { getFiltersByKeyPrefix, isFilteringOnFixedValue } from '../../util/filters'
|
||||
import { useQueryContext } from '../../query-context';
|
||||
import { useSiteContext } from '../../site-context';
|
||||
|
||||
|
||||
export default function Sources(props) {
|
||||
if (isFilteringOnFixedValue(props.query, 'source', 'Google')) {
|
||||
return <SearchTerms {...props} />
|
||||
} else if (isFilteringOnFixedValue(props.query, 'source')) {
|
||||
const [[_operation, _filterKey, clauses]] = getFiltersByKeyPrefix(props.query, "source")
|
||||
return <ReferrerList {...props} source={clauses[0]} />
|
||||
export default function Sources() {
|
||||
const { query } = useQueryContext();
|
||||
const site = useSiteContext();
|
||||
if (isFilteringOnFixedValue(query, 'source', 'Google')) {
|
||||
return <SearchTerms query={query} site={site} />
|
||||
} else if (isFilteringOnFixedValue(query, 'source')) {
|
||||
const [[_operation, _filterKey, clauses]] = getFiltersByKeyPrefix(query, "source")
|
||||
return <ReferrerList query={query} site={site} source={clauses[0]} />
|
||||
} else {
|
||||
return <SourceList {...props} />
|
||||
return <SourceList query={query} site={site} />
|
||||
}
|
||||
}
|
||||
|
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
|
||||
}
|
||||
|
||||
export function isRealTimeDashboard(query) {
|
||||
return query?.period === 'realtime'
|
||||
}
|
||||
|
||||
// Note: Currently only a single goal filter can be applied at a time.
|
||||
export function getGoalFilter(query) {
|
||||
return getFiltersByKeyPrefix(query, "goal")[0] || null
|
||||
|
200
assets/package-lock.json
generated
200
assets/package-lock.json
generated
@ -65,24 +65,27 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.21.4",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz",
|
||||
"integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/highlight": "^7.18.6"
|
||||
"@babel/highlight": "^7.24.7",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.21.5",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.7.tgz",
|
||||
"integrity": "sha512-oipXieGC3i45Y1A41t4tAqpnEZWgB/lC6Ehh6+rOviR5XWpTtMmLN+fGjz9vOiNRt0p6RtO6DtD0pdU3vpqdSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.21.5",
|
||||
"@jridgewell/gen-mapping": "^0.3.2",
|
||||
"@jridgewell/trace-mapping": "^0.3.17",
|
||||
"@babel/types": "^7.24.7",
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25",
|
||||
"jsesc": "^2.5.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -90,80 +93,92 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-environment-visitor": {
|
||||
"version": "7.21.5",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.7.tgz",
|
||||
"integrity": "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.24.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-function-name": {
|
||||
"version": "7.21.0",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.24.7.tgz",
|
||||
"integrity": "sha512-FyoJTsj/PEUWu1/TYRiXTIHc8lbw+TDYkZuoE43opPS5TrI7MyONBE1oNvfguEXAD9yhQRrVBnXdXzSLQl9XnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.20.7",
|
||||
"@babel/types": "^7.21.0"
|
||||
"@babel/template": "^7.24.7",
|
||||
"@babel/types": "^7.24.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-hoist-variables": {
|
||||
"version": "7.18.6",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.7.tgz",
|
||||
"integrity": "sha512-MJJwhkoGy5c4ehfoRyrJ/owKeMl19U54h27YYftT0o2teQ3FJ3nQUf/I3LlJsX4l3qlw7WRXUmiyajvHXoTubQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.24.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-split-export-declaration": {
|
||||
"version": "7.18.6",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz",
|
||||
"integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.18.6"
|
||||
"@babel/types": "^7.24.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.21.5",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.7.tgz",
|
||||
"integrity": "sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.19.1",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz",
|
||||
"integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/highlight": {
|
||||
"version": "7.18.6",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz",
|
||||
"integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.18.6",
|
||||
"chalk": "^2.0.0",
|
||||
"js-tokens": "^4.0.0"
|
||||
"@babel/helper-validator-identifier": "^7.24.7",
|
||||
"chalk": "^2.4.2",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.21.8",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
|
||||
"integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
},
|
||||
@ -182,32 +197,34 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.20.7",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.7.tgz",
|
||||
"integrity": "sha512-jYqfPrU9JTF0PmPy1tLYHW4Mp4KlgxJD9l2nP9fD6yT/ICi554DmrWBAEYpIelzjHf1msDP3PxJIRt/nFNfBig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.18.6",
|
||||
"@babel/parser": "^7.20.7",
|
||||
"@babel/types": "^7.20.7"
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@babel/types": "^7.24.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.21.5",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.7.tgz",
|
||||
"integrity": "sha512-yb65Ed5S/QAcewNPh0nZczy9JdYXkkAbIsEo+P7BE7yO3txAY30Y/oPa3QkQ5It3xVG2kpKMg9MsdxZaO31uKA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.21.4",
|
||||
"@babel/generator": "^7.21.5",
|
||||
"@babel/helper-environment-visitor": "^7.21.5",
|
||||
"@babel/helper-function-name": "^7.21.0",
|
||||
"@babel/helper-hoist-variables": "^7.18.6",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6",
|
||||
"@babel/parser": "^7.21.5",
|
||||
"@babel/types": "^7.21.5",
|
||||
"debug": "^4.1.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
"@babel/generator": "^7.24.7",
|
||||
"@babel/helper-environment-visitor": "^7.24.7",
|
||||
"@babel/helper-function-name": "^7.24.7",
|
||||
"@babel/helper-hoist-variables": "^7.24.7",
|
||||
"@babel/helper-split-export-declaration": "^7.24.7",
|
||||
"@babel/parser": "^7.24.7",
|
||||
"@babel/types": "^7.24.7",
|
||||
"debug": "^4.3.1",
|
||||
"globals": "^11.1.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -215,12 +232,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.21.5",
|
||||
"version": "7.24.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.7.tgz",
|
||||
"integrity": "sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.21.5",
|
||||
"@babel/helper-validator-identifier": "^7.19.1",
|
||||
"@babel/helper-string-parser": "^7.24.7",
|
||||
"@babel/helper-validator-identifier": "^7.24.7",
|
||||
"to-fast-properties": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -311,41 +329,46 @@
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.2",
|
||||
"license": "MIT",
|
||||
"version": "0.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
|
||||
"integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.0.1",
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/trace-mapping": "^0.3.9"
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.0",
|
||||
"license": "MIT",
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.1.2",
|
||||
"license": "MIT",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.4.14",
|
||||
"license": "MIT"
|
||||
"version": "1.4.15",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||
"integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg=="
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.18",
|
||||
"license": "MIT",
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "1.4.14"
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@jsonurl/jsonurl": {
|
||||
@ -740,10 +763,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"license": "MIT",
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@ -1999,8 +2023,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"license": "MIT",
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@ -2652,7 +2677,8 @@
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@ -2846,8 +2872,9 @@
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
@ -3116,14 +3143,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.6",
|
||||
"version": "3.3.7",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
|
||||
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
@ -3461,8 +3489,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.0.0",
|
||||
"license": "ISC"
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
|
||||
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew=="
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
@ -3491,7 +3520,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.4.29",
|
||||
"version": "8.4.39",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
|
||||
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@ -3506,11 +3537,10 @@
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.6",
|
||||
"picocolors": "^1.0.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.0.1",
|
||||
"source-map-js": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
@ -4246,8 +4276,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"license": "BSD-3-Clause",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
|
||||
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@ -4839,7 +4870,8 @@
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"license": "MIT",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user