Improve event metadata (#385)

* Change from dropdown to tabs for metadata breakdown

* Add meta filter to base_query

* Refactor: use base_query_w_sessions in top_referrers_for_goal

* Do not allow individual metadata filter

* Fix conversions report when combining 3 filters: goal, metadata, country

* Remember selected metadata key

* Compress conversions component
This commit is contained in:
Uku Taht 2020-10-29 15:33:37 +02:00 committed by GitHub
parent 40900c7653
commit ff515c641d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 61 additions and 82 deletions

View File

@ -48,7 +48,9 @@ function filterText(key, value, query) {
function renderFilter(history, [key, value], query) { function renderFilter(history, [key, value], query) {
function removeFilter() { function removeFilter() {
history.push({search: removeQueryParam(location.search, key)}) let newQuery = removeQueryParam(location.search, key)
if (key === 'goal') { newQuery = removeQueryParam(newQuery, 'meta') }
history.push({search: newQuery})
} }
return ( return (

View File

@ -50,13 +50,13 @@ export default class Conversions extends React.Component {
return ( return (
<div className="my-2 text-sm" key={goal.name}> <div className="my-2 text-sm" key={goal.name}>
<div className="flex items-center justify-between my-2"> <div className="flex items-center justify-between my-2">
<div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 14rem)'}}> <div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 10rem)'}}>
<Bar count={goal.count} all={this.state.goals} bg="bg-red-50" /> <Bar count={goal.count} all={this.state.goals} bg="bg-red-50" />
{this.renderGoalText(goal.name)} {this.renderGoalText(goal.name)}
</div> </div>
<div> <div>
<span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.count)}</span> <span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.count)}</span>
<span className="font-medium inline-block w-36 text-right">{numberFormatter(goal.total_count)}</span> <span className="font-medium inline-block w-20 text-right">{numberFormatter(goal.total_count)}</span>
</div> </div>
</div> </div>
{ renderMeta && <MetaBreakdown site={this.props.site} query={this.props.query} goal={goal} /> } { renderMeta && <MetaBreakdown site={this.props.site} query={this.props.query} goal={goal} /> }
@ -79,7 +79,7 @@ export default class Conversions extends React.Component {
<span>Goal</span> <span>Goal</span>
<div className="text-right"> <div className="text-right">
<span className="inline-block w-20">Uniques</span> <span className="inline-block w-20">Uniques</span>
<span className="inline-block w-36">Total conversions</span> <span className="inline-block w-20">Total</span>
</div> </div>
</div> </div>

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import Transition from "../../../transition.js";
import Bar from '../bar' import Bar from '../bar'
import numberFormatter from '../../number-formatter' import numberFormatter from '../../number-formatter'
import * as api from '../../api' import * as api from '../../api'
@ -9,31 +8,24 @@ import * as api from '../../api'
export default class MetaBreakdown extends React.Component { export default class MetaBreakdown extends React.Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.handleClick = this.handleClick.bind(this) let metaKey = props.goal.meta_keys[0]
const metaFilter = props.query.filters['meta'] this.storageKey = 'goalMetaTab__' + props.site.domain + props.goal.name
console.log(metaFilter) const storedKey = window.localStorage[this.storageKey]
const metaKey = metaFilter ? Object.keys(metaFilter)[0] : props.goal.meta_keys[0] if (props.goal.meta_keys.includes(storedKey)) {
metaKey = storedKey
}
if (props.query.filters['meta']) {
metaKey = Object.keys(props.query.filters['meta'])[0]
}
this.state = { this.state = {
loading: true, loading: true,
dropdownOpen: false,
metaKey: metaKey metaKey: metaKey
} }
} }
componentDidMount() { componentDidMount() {
this.fetchMetaBreakdown() this.fetchMetaBreakdown()
document.addEventListener('mousedown', this.handleClick, false);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleClick, false);
}
handleClick(e) {
if (this.dropDownNode && this.dropDownNode.contains(e.target)) return;
if (!this.state.dropdownOpen) return;
this.setState({dropdownOpen: false})
} }
fetchMetaBreakdown() { fetchMetaBreakdown() {
@ -49,7 +41,7 @@ export default class MetaBreakdown extends React.Component {
return ( return (
<div className="flex items-center justify-between my-2" key={value.name}> <div className="flex items-center justify-between my-2" key={value.name}>
<div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 14rem)'}}> <div className="w-full h-8 relative" style={{maxWidth: 'calc(100% - 10rem)'}}>
<Bar count={value.count} all={this.state.breakdown} bg="bg-red-50" /> <Bar count={value.count} all={this.state.breakdown} bg="bg-red-50" />
<Link to={{search: query.toString()}} style={{marginTop: '-26px'}} className="hover:underline block px-2"> <Link to={{search: query.toString()}} style={{marginTop: '-26px'}} className="hover:underline block px-2">
{ value.name } { value.name }
@ -57,36 +49,15 @@ export default class MetaBreakdown extends React.Component {
</div> </div>
<div> <div>
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.count)}</span> <span className="font-medium inline-block w-20 text-right">{numberFormatter(value.count)}</span>
<span className="font-medium inline-block w-36 text-right">{numberFormatter(value.total_count)}</span> <span className="font-medium inline-block w-20 text-right">{numberFormatter(value.total_count)}</span>
</div> </div>
</div> </div>
) )
} }
changeMetaKey(newKey) { changeMetaKey(newKey) {
this.setState({metaKey: newKey, loading: true, dropdownOpen: false}, this.fetchMetaBreakdown) window.localStorage[this.storageKey] = newKey
} this.setState({metaKey: newKey, loading: true}, this.fetchMetaBreakdown)
renderMetaKeyOption(key) {
const extraClass = key === this.state.metaKey ? 'font-medium text-gray-900' : 'hover:bg-gray-100 hover:text-gray-900 focus:outline-none focus:bg-gray-100 focus:text-gray-900'
return (
<span onClick={this.changeMetaKey.bind(this, key)} key={key} className={`cursor-pointer block truncate px-4 py-2 text-sm leading-5 text-gray-700 ${extraClass}`}>
{key}
</span>
)
}
renderDropdown() {
return (
<div className="py-1">
{ this.props.goal.meta_keys.map(this.renderMetaKeyOption.bind(this)) }
</div>
)
}
toggleDropdown() {
this.setState({dropdownOpen: !this.state.dropdownOpen})
} }
renderBody() { renderBody() {
@ -97,32 +68,24 @@ export default class MetaBreakdown extends React.Component {
} }
} }
renderPill(key) {
const isActive = this.state.metaKey === key
if (isActive) {
return <li key={key} className="inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700">{key}</li>
} else {
return <li key={key} className="hover:text-indigo-700 cursor-pointer" onClick={this.changeMetaKey.bind(this, key)}>{key}</li>
}
}
render() { render() {
return ( return (
<div className="w-full pl-6 mt-4"> <div className="w-full pl-6 mt-4">
<div className="relative"> <div className="flex items-center pb-1">
Breakdown by <span className="text-xs font-bold text-gray-600">Breakdown by:</span>
<button onClick={this.toggleDropdown.bind(this)} className="ml-1 inline-flex items-center rounded-md leading-5 font-bold text-gray-700 focus:outline-none transition ease-in-out duration-150 hover:text-gray-500 focus:border-blue-300 focus:shadow-outline-blue"> <ul className="flex font-medium text-xs text-gray-500 space-x-2 leading-5 pl-1">
{ this.state.metaKey } { this.props.goal.meta_keys.map(this.renderPill.bind(this)) }
<svg className="mt-px h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> </ul>
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
</svg>
</button>
<Transition
show={this.state.dropdownOpen}
enter="transition ease-out duration-100 transform"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="transition ease-in duration-75 transform"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div className="z-10 origin-top-left absolute left-0 mt-2 w-64 rounded-md shadow-lg" ref={node => this.dropDownNode = node} >
<div className="rounded-md bg-white shadow-xs">
{ this.renderDropdown() }
</div>
</div>
</Transition>
</div> </div>
{ this.renderBody() } { this.renderBody() }
</div> </div>

View File

@ -291,10 +291,9 @@ export default class Devices extends React.Component {
renderPill(name, mode) { renderPill(name, mode) {
const isActive = this.state.mode === mode const isActive = this.state.mode === mode
const extraClass = name === 'OS' ? '' : ' border-r border-gray-300'
if (isActive) { if (isActive) {
return <li className="inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700" onClick={this.setMode(mode)}>{name}</li> return <li className="inline-block h-5 text-indigo-700 font-bold border-b-2 border-indigo-700">{name}</li>
} else { } else {
return <li className="hover:text-indigo-700 cursor-pointer" onClick={this.setMode(mode)}>{name}</li> return <li className="hover:text-indigo-700 cursor-pointer" onClick={this.setMode(mode)}>{name}</li>
} }

View File

@ -171,14 +171,8 @@ defmodule Plausible.Stats.Clickhouse do
def top_referrers_for_goal(site, query, limit, page) do def top_referrers_for_goal(site, query, limit, page) do
offset = (page - 1) * limit offset = (page - 1) * limit
converted_sessions =
from(e in base_query(site, query),
select: %{session_id: e.session_id})
ClickhouseRepo.all( ClickhouseRepo.all(
from s in Plausible.ClickhouseSession, from s in base_query_w_sessions(site, query),
join: cs in subquery(converted_sessions),
on: s.session_id == cs.session_id,
where: s.referrer_source != "", where: s.referrer_source != "",
group_by: s.referrer_source, group_by: s.referrer_source,
order_by: [desc: fragment("count")], order_by: [desc: fragment("count")],
@ -611,7 +605,7 @@ defmodule Plausible.Stats.Clickhouse do
%{goal => [key]} %{goal => [key]}
else else
ClickhouseRepo.all( ClickhouseRepo.all(
from [e, meta] in base_query_w_sessions_bare(site, query), from [e, meta: meta] in base_query_w_sessions_bare(site, query),
select: {e.name, meta.key}, select: {e.name, meta.key},
distinct: true distinct: true
) |> Enum.reduce(%{}, fn {goal_name, meta_key}, acc -> ) |> Enum.reduce(%{}, fn {goal_name, meta_key}, acc ->
@ -647,7 +641,7 @@ defmodule Plausible.Stats.Clickhouse do
) )
else else
ClickhouseRepo.all( ClickhouseRepo.all(
from [e, meta] in base_query_w_sessions(site, query), from [e, meta: meta] in base_query_w_sessions(site, query),
group_by: meta.value, group_by: meta.value,
order_by: [desc: fragment("count")], order_by: [desc: fragment("count")],
select: %{ select: %{
@ -857,7 +851,7 @@ defmodule Plausible.Stats.Clickhouse do
q q
end end
if query.filters["page"] do q = if query.filters["page"] do
page = query.filters["page"] page = query.filters["page"]
from(e in q, where: e.pathname == ^page) from(e in q, where: e.pathname == ^page)
else else
@ -876,6 +870,7 @@ defmodule Plausible.Stats.Clickhouse do
from( from(
e in q, e in q,
inner_lateral_join: meta in fragment("meta as m"), inner_lateral_join: meta in fragment("meta as m"),
as: :meta,
where: meta.key == ^key and meta.value == ^val, where: meta.key == ^key and meta.value == ^val,
) )
end end
@ -1075,6 +1070,26 @@ defmodule Plausible.Stats.Clickhouse do
q q
end end
q = if query.filters["meta"] do
[{key, val}] = query.filters["meta"] |> Enum.into([])
if val == "(none)" do
from(
e in q,
where: fragment("not has(meta.key, ?)", ^key)
)
else
from(
e in q,
inner_lateral_join: meta in fragment("meta as m"),
where: meta.key == ^key and meta.value == ^val,
)
end
else
q
end
q = q =
if path do if path do
from(e in q, where: e.pathname == ^path) from(e in q, where: e.pathname == ^path)