analytics/assets/js/dashboard/stats/behaviours/index.js
RobertJoonas ba19f9530e
UI groundwork: Conversions to Behaviors (#3005)
* copy relevant files from b2ace16540

* make it work and set site.funnels to empty list

* make Behaviours a functional component

* add UI for a setup hint and always display conversions by default

* cherry-pick migration commit

* update site schema with new fields

* backend: implement disable-feature action

* switch between tabs in the behaviors section

Introduces template components to build props and funnels on. Both
only show a setup notice atm, and both are behind feature flags.

* extend API for disabling props and funnels

* render feature setup note directly from the Behaviours component

* fix UI behavior when features are hidden

* update setup notices

* add conversions feature switch to Site Settings > Goals

* mix format

* remove IO.inspect

* change setup notice - use buttons + popup confirmation

* optimize for light mode

* restrict access to setup notices

* some styling improvements

* allow super-admins to enable/disable features

* only show conversions (last 30min) in realtime mode

* use shorter display names for tabs

* optimize for mobile screens

* note about sending custom events

* changelog update + fix CI

* change HTTP verb for the disable-feature action

* change UI label for show/hide goals
2023-06-13 08:16:38 +02:00

251 lines
8.0 KiB
JavaScript

import React, { Fragment, useState, useEffect } from 'react'
import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import classNames from 'classnames'
import * as storage from '../../util/storage'
import Conversions from './conversions'
import Funnel from './funnel'
import { FeatureSetupNotice } from '../../components/notice'
const ACTIVE_CLASS = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold active-prop-heading truncate text-left'
const DEFAULT_CLASS = 'hover:text-indigo-600 cursor-pointer truncate text-left'
export const CONVERSIONS = 'conversions'
export const FUNNELS = 'funnels'
export const PROPS = 'props'
export const sectionTitles = {
[CONVERSIONS]: 'Goal Conversions',
[FUNNELS]: 'Funnels',
[PROPS]: 'Custom Properties'
}
export default function Behaviours(props) {
const site = props.site
const adminAccess = ['owner', 'admin', 'super_admin'].includes(props.currentUserRole)
const tabKey = `behavioursTab__${site.domain}`
const funnelKey = `behavioursTabFunnel__${site.domain}`
const [enabledModes, setEnabledModes] = useState(getEnabledModes())
const [mode, setMode] = useState(defaultMode())
const [funnelNames, _setFunnelNames] = useState(site.funnels.map(({ name }) => name))
const [selectedFunnel, setSelectedFunnel] = useState(storage.getItem(funnelKey))
useEffect(() => {
setMode(defaultMode())
}, [enabledModes])
function disableMode(mode) {
setEnabledModes(enabledModes.filter((m) => { return m !== mode }))
}
function setFunnel(selectedFunnel) {
return () => {
storage.setItem(tabKey, FUNNELS)
storage.setItem(funnelKey, selectedFunnel)
setMode(FUNNELS)
setSelectedFunnel(selectedFunnel)
}
}
function hasFunnels() {
return site.funnels.length > 0
}
function tabFunnelPicker() {
return <Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="inline-flex justify-between focus:outline-none">
<span className={(mode == FUNNELS) ? ACTIVE_CLASS : DEFAULT_CLASS}>Funnels</span>
<ChevronDownIcon className="-mr-1 ml-1 h-4 w-4" aria-hidden="true" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="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">
<div className="py-1">
{funnelNames.map((funnelName) => {
return (
<Menu.Item key={funnelName}>
{({ active }) => (
<span
onClick={setFunnel(funnelName)}
className={classNames(
active ? 'bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-gray-200 cursor-pointer' : 'text-gray-700 dark:text-gray-200',
'block px-4 py-2 text-sm',
mode === funnelName ? 'font-bold' : ''
)}
>
{funnelName}
</span>
)}
</Menu.Item>
)
})}
</div>
</Menu.Items>
</Transition>
</Menu>
}
function tabSwitcher(toMode, displayName) {
const className = classNames({ [ACTIVE_CLASS]: mode == toMode, [DEFAULT_CLASS]: mode !== toMode })
const setTab = () => {
storage.setItem(tabKey, toMode)
setMode(toMode)
}
return (
<div className={className} onClick={setTab}>
{displayName}
</div>
)
}
function tabs() {
return (
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
{isEnabled(CONVERSIONS) && tabSwitcher(CONVERSIONS, 'Goals')}
{isEnabled(FUNNELS) && (hasFunnels() ? tabFunnelPicker() : tabSwitcher(FUNNELS, 'Funnels'))}
{isEnabled(PROPS) && tabSwitcher(PROPS, 'Properties')}
</div>
)
}
function renderConversions() {
if (site.hasGoals) { return <Conversions site={site} query={props.query} /> }
else if (adminAccess) {
return (
<FeatureSetupNotice
site={site}
feature={CONVERSIONS}
shortFeatureName={'goals'}
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.'}
settingsLink={`/${encodeURIComponent(site.domain)}/settings/goals`}
onHideAction={onHideAction(CONVERSIONS)}
/>
)
}
else { return noDataYet() }
}
function renderFunnels() {
if (selectedFunnel) { return <Funnel site={site} query={props.query} funnelName={selectedFunnel} /> }
else if (adminAccess) {
return (
<FeatureSetupNotice
site={site}
feature={FUNNELS}
shortFeatureName={'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.'}
settingsLink={`/${encodeURIComponent(site.domain)}/settings/funnels`}
onHideAction={onHideAction(FUNNELS)}
/>
)
}
else { return noDataYet() }
}
function renderProps() {
if (adminAccess) {
return (
<FeatureSetupNotice
site={site}
feature={PROPS}
shortFeatureName={'props'}
title={'No custom properties found'}
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.'}
settingsLink={`/${encodeURIComponent(site.domain)}/settings/props`}
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 onHideAction(mode) {
return () => { disableMode(mode) }
}
function renderContent() {
switch (mode) {
case CONVERSIONS:
return renderConversions()
case FUNNELS:
return renderFunnels()
case PROPS:
return renderProps()
}
}
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(FUNNELS)) { return FUNNELS }
return PROPS
}
function getEnabledModes() {
let enabledModes = []
if (site.conversionsEnabled) {
enabledModes.push(CONVERSIONS)
}
if (site.funnelsEnabled && !isRealtime() && site.flags.funnels) {
enabledModes.push(FUNNELS)
}
if (site.propsEnabled && !isRealtime() && site.flags.props) {
enabledModes.push(PROPS)
}
return enabledModes
}
function isEnabled(mode) {
return enabledModes.includes(mode)
}
function isRealtime() {
return props.query.period === 'realtime'
}
if (mode) {
return (
<div className="items-start justify-between block w-full mt-6 md:flex">
<div className="w-full p-4 bg-white rounded shadow-xl dark:bg-gray-825">
<div className="flex justify-between w-full">
<h3 className="font-bold dark:text-gray-100">
{sectionTitles[mode] + (isRealtime() ? ' (last 30min)' : '')}
</h3>
{tabs()}
</div>
{renderContent()}
</div>
</div>
)
} else {
return null
}
}