mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 01:22:15 +03:00
9ce9264144
* chore(docker): improve repeat contributions workflow * This change adds two new commands to gracefully stop and remove the Postgres and Clickhouse docker containers. To do so, it also gives them a recognizable name. * Additionally, the Postgres container is updated to use a named volume for its data. This lower friction for repeat contributions where one would otherwise sign up and activate their accounts again and again each time. * Improve bar component to work in a variety of situations This fixes two issues from #972 - Goals area has display issues depending on the name of your custom events - It is not possible to view labels for outbound links, 404 and custom props * Update changelog entry * Move content to children for Bar component * Remove redundant fallback width * Fix text color on root conversion texts Hyperlinks (<a>) inherit their color. In the case of the root conversion text, we needed to specify the color somewhere. The same color as the breakdown texts has been chosen. The Bar component no longer needs to take in text classes.
156 lines
4.9 KiB
JavaScript
156 lines
4.9 KiB
JavaScript
import React from 'react';
|
|
import { Link } from 'react-router-dom'
|
|
|
|
import * as storage from '../../storage'
|
|
import Bar from '../bar'
|
|
import numberFormatter from '../../number-formatter'
|
|
import * as api from '../../api'
|
|
|
|
const MOBILE_UPPER_WIDTH = 767
|
|
const DEFAULT_WIDTH = 1080
|
|
|
|
export default class PropertyBreakdown extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
let propKey = props.goal.prop_names[0]
|
|
this.storageKey = 'goalPropTab__' + props.site.domain + props.goal.name
|
|
const storedKey = storage.getItem(this.storageKey)
|
|
if (props.goal.prop_names.includes(storedKey)) {
|
|
propKey = storedKey
|
|
}
|
|
if (props.query.filters['props']) {
|
|
propKey = Object.keys(props.query.filters['props'])[0]
|
|
}
|
|
|
|
this.state = {
|
|
loading: true,
|
|
propKey: propKey,
|
|
viewport: DEFAULT_WIDTH,
|
|
}
|
|
|
|
this.handleResize = this.handleResize.bind(this);
|
|
}
|
|
|
|
componentDidMount() {
|
|
window.addEventListener('resize', this.handleResize, false);
|
|
|
|
this.handleResize();
|
|
this.fetchPropBreakdown()
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener('resize', this.handleResize, false);
|
|
}
|
|
|
|
handleResize() {
|
|
this.setState({ viewport: window.innerWidth });
|
|
}
|
|
|
|
getBarMaxWidth() {
|
|
const { viewport } = this.state;
|
|
return viewport > MOBILE_UPPER_WIDTH ? "16rem" : "10rem";
|
|
}
|
|
|
|
fetchPropBreakdown() {
|
|
if (this.props.query.filters['goal']) {
|
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/property/${encodeURIComponent(this.state.propKey)}`, this.props.query)
|
|
.then((res) => this.setState({loading: false, breakdown: res}))
|
|
}
|
|
}
|
|
|
|
renderUrl(value) {
|
|
if (value.is_url) {
|
|
return (
|
|
<a target="_blank" href={value.name} className="hidden group-hover:block">
|
|
<svg className="inline h-4 w-4 ml-1 -mt-1 text-gray-600 dark:text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path d="M11 3a1 1 0 100 2h2.586l-6.293 6.293a1 1 0 101.414 1.414L15 6.414V9a1 1 0 102 0V4a1 1 0 00-1-1h-5z"></path><path d="M5 5a2 2 0 00-2 2v8a2 2 0 002 2h8a2 2 0 002-2v-3a1 1 0 10-2 0v3H5V7h3a1 1 0 000-2H5z"></path></svg>
|
|
</a>
|
|
)
|
|
}
|
|
return null
|
|
}
|
|
|
|
renderPropContent(value, query) {
|
|
return (
|
|
<span className="flex px-2 py-1.5 group dark:text-gray-300 relative z-9 break-words">
|
|
<Link
|
|
to={{pathname: window.location.pathname, search: query.toString()}}
|
|
className="hover:underline block"
|
|
>
|
|
{ value.name }
|
|
</Link>
|
|
{ this.renderUrl(value) }
|
|
</span>
|
|
)
|
|
}
|
|
|
|
renderPropValue(value) {
|
|
const query = new URLSearchParams(window.location.search)
|
|
query.set('props', JSON.stringify({[this.state.propKey]: value.name}))
|
|
const { viewport } = this.state;
|
|
|
|
return (
|
|
<div className="flex items-center justify-between my-2" key={value.name}>
|
|
<Bar
|
|
count={value.count}
|
|
all={this.state.breakdown}
|
|
bg="bg-red-50 dark:bg-gray-500 dark:bg-opacity-15"
|
|
maxWidthDeduction={this.getBarMaxWidth()}
|
|
>
|
|
{this.renderPropContent(value, query)}
|
|
</Bar>
|
|
<div className="dark:text-gray-200">
|
|
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.count)}</span>
|
|
{
|
|
viewport > MOBILE_UPPER_WIDTH ?
|
|
(
|
|
<span
|
|
className="font-medium inline-block w-20 text-right"
|
|
>{numberFormatter(value.total_count)}
|
|
</span>
|
|
)
|
|
: null
|
|
}
|
|
<span className="font-medium inline-block w-20 text-right">{numberFormatter(value.conversion_rate)}%</span>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
changePropKey(newKey) {
|
|
storage.setItem(this.storageKey, newKey)
|
|
this.setState({propKey: newKey, loading: true}, this.fetchPropBreakdown)
|
|
}
|
|
|
|
renderBody() {
|
|
if (this.state.loading) {
|
|
return <div className="px-4 py-2"><div className="loading sm mx-auto"><div></div></div></div>
|
|
} else {
|
|
return this.state.breakdown.map((propValue) => this.renderPropValue(propValue))
|
|
}
|
|
}
|
|
|
|
renderPill(key) {
|
|
const isActive = this.state.propKey === key
|
|
|
|
if (isActive) {
|
|
return <li key={key} className="inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold border-b-2 border-indigo-700 dark:border-indigo-500 ">{key}</li>
|
|
} else {
|
|
return <li key={key} className="hover:text-indigo-600 cursor-pointer" onClick={this.changePropKey.bind(this, key)}>{key}</li>
|
|
}
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div className="w-full pl-6 mt-4">
|
|
<div className="flex items-center pb-1">
|
|
<span className="text-xs font-bold text-gray-600 dark:text-gray-300">Breakdown by:</span>
|
|
<ul className="flex font-medium text-xs text-gray-500 dark:text-gray-400 space-x-2 leading-5 pl-1">
|
|
{ this.props.goal.prop_names.map(this.renderPill.bind(this)) }
|
|
</ul>
|
|
</div>
|
|
{ this.renderBody() }
|
|
</div>
|
|
)
|
|
}
|
|
}
|