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

186 lines
5.2 KiB
JavaScript

import React, { useCallback, useEffect, useState } from 'react'
import ListReport, { MIN_HEIGHT } from '../reports/list'
import Combobox from '../../components/combobox'
import * as metrics from '../reports/metrics'
import * as api from '../../api'
import * as url from '../../util/url'
import * as storage from '../../util/storage'
import {
EVENT_PROPS_PREFIX,
getGoalFilter,
FILTER_OPERATIONS,
hasConversionGoalFilter
} from '../../util/filters'
import classNames from 'classnames'
import { useQueryContext } from '../../query-context'
import { useSiteContext } from '../../site-context'
import { customPropsRoute } from '../../router'
export default function Properties({ afterFetchData }) {
const { query } = useQueryContext()
const site = useSiteContext()
const propKeyStorageName = `prop_key__${site.domain}`
const propKeyStorageNameForGoal = () => {
const [_operation, _filterKey, [goal]] = getGoalFilter(query)
return `${goal}__prop_key__${site.domain}`
}
const [propKey, setPropKey] = useState(null)
const [propKeyLoading, setPropKeyLoading] = useState(true)
function singleGoalFilterApplied() {
const goalFilter = getGoalFilter(query)
if (goalFilter) {
const [operation, _filterKey, clauses] = goalFilter
return operation === FILTER_OPERATIONS.is && clauses.length === 1
} else {
return false
}
}
useEffect(() => {
setPropKeyLoading(true)
setPropKey(null)
fetchPropKeyOptions()('').then((propKeys) => {
const propKeyValues = propKeys.map((entry) => entry.value)
if (propKeyValues.length > 0) {
const storedPropKey = getPropKeyFromStorage()
if (propKeyValues.includes(storedPropKey)) {
setPropKey(storedPropKey)
} else {
setPropKey(propKeys[0].value)
}
}
setPropKeyLoading(false)
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
function getPropKeyFromStorage() {
if (singleGoalFilterApplied()) {
const storedForGoal = storage.getItem(propKeyStorageNameForGoal())
if (storedForGoal) {
return storedForGoal
}
}
return storage.getItem(propKeyStorageName)
}
function fetchProps() {
return api.get(
url.apiPath(site, `/custom-prop-values/${encodeURIComponent(propKey)}`),
query
)
}
const fetchPropKeyOptions = useCallback(() => {
return (input) => {
return api.get(url.apiPath(site, '/suggestions/prop_key'), query, {
q: input.trim()
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query])
function onPropKeySelect() {
return (selectedOptions) => {
const newPropKey =
selectedOptions.length === 0 ? null : selectedOptions[0].value
if (newPropKey) {
const storageName = singleGoalFilterApplied()
? propKeyStorageNameForGoal()
: propKeyStorageName
storage.setItem(storageName, newPropKey)
}
setPropKey(newPropKey)
}
}
/*global BUILD_EXTRA*/
function chooseMetrics() {
return [
metrics.createVisitors({
renderLabel: (_query) => 'Visitors',
meta: { plot: true }
}),
metrics.createEvents({
renderLabel: (_query) => 'Events',
meta: { hiddenOnMobile: true }
}),
hasConversionGoalFilter(query) && metrics.createConversionRate(),
!hasConversionGoalFilter(query) && metrics.createPercentage(),
BUILD_EXTRA &&
metrics.createTotalRevenue({ meta: { hiddenOnMobile: true } }),
BUILD_EXTRA &&
metrics.createAverageRevenue({ meta: { hiddenOnMobile: true } })
].filter((metric) => !!metric)
}
function renderBreakdown() {
return (
<ListReport
fetchData={fetchProps}
afterFetchData={afterFetchData}
getFilterInfo={getFilterInfo}
keyLabel={propKey}
metrics={chooseMetrics()}
detailsLinkProps={{
path: customPropsRoute.path,
params: { propKey },
search: (search) => search
}}
maybeHideDetails={true}
color="bg-red-50"
colMinWidth={90}
/>
)
}
const getFilterInfo = (listItem) => ({
prefix: `${EVENT_PROPS_PREFIX}${propKey}`,
filter: ['is', `${EVENT_PROPS_PREFIX}${propKey}`, [listItem.name]]
})
const comboboxDisabled = !propKeyLoading && !propKey
const comboboxPlaceholder = comboboxDisabled
? 'No custom properties found'
: ''
const comboboxValues = propKey ? [{ value: propKey, label: propKey }] : []
const boxClass = classNames(
'pl-2 pr-8 py-1 bg-transparent dark:text-gray-300 rounded-md shadow-sm border border-gray-300 dark:bg-gray-750 dark:border-gray-750',
{
'pointer-events-none': comboboxDisabled
}
)
const COMBOBOX_HEIGHT = 40
return (
<div
className="w-full mt-4"
style={{ minHeight: `${COMBOBOX_HEIGHT + MIN_HEIGHT}px` }}
>
<div style={{ minHeight: `${COMBOBOX_HEIGHT}px` }}>
<Combobox
boxClass={boxClass}
forceLoading={propKeyLoading}
fetchOptions={fetchPropKeyOptions()}
singleOption={true}
values={comboboxValues}
onSelect={onPropKeySelect()}
placeholder={comboboxPlaceholder}
/>
</div>
{propKey && renderBreakdown()}
</div>
)
}