analytics/assets/js/dashboard/stats/behaviours/index.js

404 lines
11 KiB
JavaScript

import React, { useState, useEffect, useCallback } from 'react'
import * as storage from '../../util/storage'
import ImportedQueryUnsupportedWarning from '../imported-query-unsupported-warning'
import GoalConversions, {
specialTitleWhenGoalFilter,
SPECIAL_GOALS
} from './goal-conversions'
import Properties from './props'
import { FeatureSetupNotice } from '../../components/notice'
import { hasConversionGoalFilter } from '../../util/filters'
import { useSiteContext } from '../../site-context'
import { useQueryContext } from '../../query-context'
import { useUserContext } from '../../user-context'
import { DropdownTabButton, TabButton, TabWrapper } from '../../components/tabs'
/*global BUILD_EXTRA*/
/*global require*/
function maybeRequire() {
if (BUILD_EXTRA) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
return require('../../extra/funnel')
} else {
return { default: null }
}
}
const Funnel = maybeRequire().default
export const CONVERSIONS = 'conversions'
export const PROPS = 'props'
export const FUNNELS = 'funnels'
export const sectionTitles = {
[CONVERSIONS]: 'Goal conversions',
[PROPS]: 'Custom properties',
[FUNNELS]: 'Funnels'
}
export default function Behaviours({ importedDataInView }) {
const { query } = useQueryContext()
const site = useSiteContext()
const user = useUserContext()
const adminAccess = ['owner', 'admin', 'editor', 'super_admin'].includes(
user.role
)
const tabKey = storage.getDomainScopedStorageKey('behavioursTab', site.domain)
const funnelKey = storage.getDomainScopedStorageKey(
'behavioursTabFunnel',
site.domain
)
const [enabledModes, setEnabledModes] = useState(getEnabledModes())
const [mode, setMode] = useState(defaultMode())
const [loading, setLoading] = useState(true)
const [selectedFunnel, setSelectedFunnel] = useState(defaultSelectedFunnel())
const [showingPropsForGoalFilter, setShowingPropsForGoalFilter] =
useState(false)
const [skipImportedReason, setSkipImportedReason] = useState(null)
const onGoalFilterClick = useCallback((e) => {
const goalName = e.target.innerHTML
const isSpecialGoal = Object.keys(SPECIAL_GOALS).includes(goalName)
const isPageviewGoal = goalName.startsWith('Visit ')
if (
!isSpecialGoal &&
!isPageviewGoal &&
enabledModes.includes(PROPS) &&
site.hasProps
) {
setShowingPropsForGoalFilter(true)
setMode(PROPS)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
const justRemovedGoalFilter = !hasConversionGoalFilter(query)
if (mode === PROPS && justRemovedGoalFilter && showingPropsForGoalFilter) {
setShowingPropsForGoalFilter(false)
setMode(CONVERSIONS)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [hasConversionGoalFilter(query)])
useEffect(() => {
setMode(defaultMode())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [enabledModes])
useEffect(() => setLoading(true), [query, mode])
function disableMode(mode) {
setEnabledModes(
enabledModes.filter((m) => {
return m !== mode
})
)
}
function setFunnelFactory(selectedFunnelName) {
return () => {
storage.setItem(tabKey, FUNNELS)
storage.setItem(funnelKey, selectedFunnelName)
setMode(FUNNELS)
setSelectedFunnel(selectedFunnelName)
}
}
function defaultSelectedFunnel() {
const stored = storage.getItem(funnelKey)
const storedExists = stored && site.funnels.some((f) => f.name === stored)
if (storedExists) {
return stored
} else if (site.funnels.length > 0) {
const firstAvailable = site.funnels[0].name
storage.setItem(funnelKey, firstAvailable)
return firstAvailable
}
}
function setTabFactory(tab) {
return () => {
storage.setItem(tabKey, tab)
setMode(tab)
}
}
function afterFetchData(apiResponse) {
setLoading(false)
setSkipImportedReason(apiResponse.skip_imported_reason)
}
function renderConversions() {
if (site.hasGoals) {
return (
<GoalConversions
onGoalFilterClick={onGoalFilterClick}
afterFetchData={afterFetchData}
/>
)
} else if (adminAccess) {
return (
<FeatureSetupNotice
feature={CONVERSIONS}
title={'Measure how often visitors complete specific actions'}
info={
'Goals allow you to track registrations, button clicks, form completions, external link clicks, file downloads, 404 error pages and more.'
}
callToAction={{
action: 'Set up goals',
link: `/${encodeURIComponent(site.domain)}/settings/goals`
}}
onHideAction={onHideAction(CONVERSIONS)}
/>
)
} else {
return noDataYet()
}
}
function renderFunnels() {
if (Funnel === null) {
return featureUnavailable()
} else if (Funnel && selectedFunnel && site.funnelsAvailable) {
return <Funnel funnelName={selectedFunnel} />
} else if (Funnel && adminAccess) {
let callToAction
if (site.funnelsAvailable) {
callToAction = {
action: 'Set up funnels',
link: `/${encodeURIComponent(site.domain)}/settings/funnels`
}
} else {
callToAction = { action: 'Upgrade', link: '/billing/choose-plan' }
}
return (
<FeatureSetupNotice
feature={FUNNELS}
title={'Follow the visitor journey from entry to conversion'}
info={
'Funnels allow you to analyze the user flow through your website, uncover possible issues, optimize your site and increase the conversion rate.'
}
callToAction={callToAction}
onHideAction={onHideAction(FUNNELS)}
/>
)
} else {
return noDataYet()
}
}
function renderProps() {
if (site.hasProps && site.propsAvailable) {
return <Properties afterFetchData={afterFetchData} />
} else if (adminAccess) {
let callToAction
if (site.propsAvailable) {
callToAction = {
action: 'Set up props',
link: `/${encodeURIComponent(site.domain)}/settings/properties`
}
} else {
callToAction = { action: 'Upgrade', link: '/billing/choose-plan' }
}
return (
<FeatureSetupNotice
feature={PROPS}
title={'Send custom data to create your own metrics'}
info={
"You can attach custom properties when sending a pageview or event. This allows you to create custom metrics and analyze stats we don't track automatically."
}
callToAction={callToAction}
onHideAction={onHideAction(PROPS)}
/>
)
} else {
return noDataYet()
}
}
function noDataYet() {
return (
<div className="font-medium text-gray-500 dark:text-gray-400 py-12 text-center">
No data yet
</div>
)
}
function featureUnavailable() {
return (
<div className="font-medium text-gray-500 dark:text-gray-400 py-12 text-center">
This feature is unavailable
</div>
)
}
function onHideAction(mode) {
return () => {
disableMode(mode)
}
}
function renderContent() {
switch (mode) {
case CONVERSIONS:
return renderConversions()
case PROPS:
return renderProps()
case FUNNELS:
return renderFunnels()
}
}
function defaultMode() {
if (enabledModes.length === 0) {
return null
}
const storedMode = storage.getItem(tabKey)
if (storedMode && enabledModes.includes(storedMode)) {
return storedMode
}
if (enabledModes.includes(CONVERSIONS)) {
return CONVERSIONS
}
if (enabledModes.includes(PROPS)) {
return PROPS
}
return FUNNELS
}
function getEnabledModes() {
let enabledModes = []
for (const feature of Object.keys(sectionTitles)) {
const isOptedOut = site[feature + 'OptedOut']
const isAvailable = site[feature + 'Available'] !== false
// 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 && user.role !== 'owner'
if (!isOptedOut && !callToActionIsMissing) {
enabledModes.push(feature)
}
}
return enabledModes
}
function isEnabled(mode) {
return enabledModes.includes(mode)
}
function isRealtime() {
return query.period === 'realtime'
}
function sectionTitle() {
if (mode === CONVERSIONS) {
return specialTitleWhenGoalFilter(query, sectionTitles[mode])
} else {
return sectionTitles[mode]
}
}
function renderImportedQueryUnsupportedWarning() {
if (mode === CONVERSIONS) {
return (
<ImportedQueryUnsupportedWarning
loading={loading}
skipImportedReason={skipImportedReason}
/>
)
} else if (mode === PROPS) {
return (
<ImportedQueryUnsupportedWarning
loading={loading}
skipImportedReason={skipImportedReason}
message="Imported data is unavailable in this view"
/>
)
} else {
return (
<ImportedQueryUnsupportedWarning
altCondition={importedDataInView}
message="Imported data is unavailable in this view"
/>
)
}
}
if (!mode) {
return null
}
return (
<div className="items-start justify-between block w-full mt-6 md:flex relative">
<div className="w-full p-4 bg-white rounded-md shadow-sm dark:bg-gray-900">
<div className="flex justify-between w-full">
<div className="flex gap-x-1">
<h3 className="font-bold dark:text-gray-100">
{sectionTitle() + (isRealtime() ? ' (last 30min)' : '')}
</h3>
{renderImportedQueryUnsupportedWarning()}
</div>
<TabWrapper>
{isEnabled(CONVERSIONS) && (
<TabButton
active={mode === CONVERSIONS}
onClick={setTabFactory(CONVERSIONS)}
>
Goals
</TabButton>
)}
{isEnabled(PROPS) && (
<TabButton active={mode === PROPS} onClick={setTabFactory(PROPS)}>
Properties
</TabButton>
)}
{isEnabled(FUNNELS) &&
Funnel &&
(site.funnels.length > 0 && site.funnelsAvailable ? (
<DropdownTabButton
className="md:relative"
transitionClassName="md:left-auto md:w-96 md:origin-top-right"
active={mode === FUNNELS}
options={site.funnels.map(({ name }) => ({
label: name,
onClick: setFunnelFactory(name),
selected: mode === FUNNELS && selectedFunnel === name
}))}
collectionTitle="Funnels"
searchable={true}
>
Funnels
</DropdownTabButton>
) : (
<TabButton
active={mode === FUNNELS}
onClick={setTabFactory(FUNNELS)}
>
Funnels
</TabButton>
))}
</TabWrapper>
</div>
{renderContent()}
</div>
</div>
)
}