mirror of
https://github.com/plausible/analytics.git
synced 2024-12-22 17:11:36 +03:00
5ee7445375
* 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. * Format countries modal * Remove unused imports * Run ESLint and make related fixes * ESlint formatting for entry pages modal * WIP: proof of concept for scrollable modals on mobile * Revert "Merge branch 'feature/details-modal-mobile' of github.com:hirusi/analytics" This reverts commit87e9fb92d0
, reversing changes made tobeea6a07c7
. * Fix issue with details button not sticking to the bottom On "All Sources", the details button wouldn't stick to the bottom when there are too few sources for the date period selected. This was due to a missing class.
280 lines
9.1 KiB
JavaScript
280 lines
9.1 KiB
JavaScript
import React from 'react';
|
|
import { Link } from 'react-router-dom'
|
|
import FlipMove from 'react-flip-move';
|
|
|
|
import * as storage from '../../storage'
|
|
import FadeIn from '../../fade-in'
|
|
import Bar from '../bar'
|
|
import MoreLink from '../more-link'
|
|
import numberFormatter from '../../number-formatter'
|
|
import * as api from '../../api'
|
|
import LazyLoader from '../../lazy-loader'
|
|
|
|
class AllSources extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
this.onVisible = this.onVisible.bind(this)
|
|
this.state = {loading: true}
|
|
}
|
|
|
|
onVisible() {
|
|
this.fetchReferrers()
|
|
if (this.props.timer) this.props.timer.onTick(this.fetchReferrers.bind(this))
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.props.query !== prevProps.query) {
|
|
this.setState({loading: true, referrers: null})
|
|
this.fetchReferrers()
|
|
}
|
|
}
|
|
|
|
showNoRef() {
|
|
return this.props.query.period === 'realtime'
|
|
}
|
|
|
|
fetchReferrers() {
|
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/sources`, this.props.query, {show_noref: this.showNoRef()})
|
|
.then((res) => this.setState({loading: false, referrers: res}))
|
|
}
|
|
|
|
renderReferrer(referrer) {
|
|
const query = new URLSearchParams(window.location.search)
|
|
query.set('source', referrer.name)
|
|
|
|
return (
|
|
<div
|
|
className="flex items-center justify-between my-1 text-sm"
|
|
key={referrer.name}
|
|
>
|
|
<Bar
|
|
count={referrer.count}
|
|
all={this.state.referrers}
|
|
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
|
maxWidthDeduction="4rem"
|
|
>
|
|
<span className="flex px-2 py-1.5 dark:text-gray-300 relative z-9 break-all">
|
|
<Link
|
|
className="md:truncate block hover:underline"
|
|
to={{search: query.toString()}}
|
|
>
|
|
<img
|
|
src={`/favicon/sources/${encodeURIComponent(referrer.name)}`}
|
|
className="inline w-4 h-4 mr-2 -mt-px align-middle"
|
|
/>
|
|
{ referrer.name }
|
|
</Link>
|
|
</span>
|
|
</Bar>
|
|
<span className="font-medium dark:text-gray-200">{numberFormatter(referrer.count)}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
label() {
|
|
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
|
}
|
|
|
|
renderList() {
|
|
if (this.state.referrers && this.state.referrers.length > 0) {
|
|
return (
|
|
<React.Fragment>
|
|
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500">
|
|
<span>Source</span>
|
|
<span>{this.label()}</span>
|
|
</div>
|
|
|
|
<FlipMove className="flex-grow">
|
|
{this.state.referrers.map(this.renderReferrer.bind(this))}
|
|
</FlipMove>
|
|
<MoreLink site={this.props.site} list={this.state.referrers} endpoint="sources" />
|
|
</React.Fragment>
|
|
)
|
|
} else {
|
|
return <div className="font-medium text-center text-gray-500 mt-44">No data yet</div>
|
|
}
|
|
}
|
|
|
|
renderContent() {
|
|
return (
|
|
<LazyLoader className="flex flex-col flex-grow" onVisible={this.onVisible}>
|
|
<div id="sources" className="flex justify-between w-full">
|
|
<h3 className="font-bold dark:text-gray-100">Top Sources</h3>
|
|
{ this.props.renderTabs() }
|
|
</div>
|
|
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
|
|
<FadeIn show={!this.state.loading} className="flex flex-col flex-grow">
|
|
{ this.renderList() }
|
|
</FadeIn>
|
|
</LazyLoader>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div
|
|
className="relative p-4 bg-white rounded shadow-xl stats-item flex flex-col mt-6 w-full dark:bg-gray-825"
|
|
>
|
|
{ this.renderContent() }
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
const UTM_TAGS = {
|
|
utm_medium: {label: 'UTM Medium', endpoint: 'utm_mediums'},
|
|
utm_source: {label: 'UTM Source', endpoint: 'utm_sources'},
|
|
utm_campaign: {label: 'UTM Campaign', endpoint: 'utm_campaigns'},
|
|
}
|
|
|
|
class UTMSources extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
this.state = {loading: true}
|
|
}
|
|
|
|
componentDidMount() {
|
|
this.fetchReferrers()
|
|
if (this.props.timer) this.props.timer.onTick(this.fetchReferrers.bind(this))
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (this.props.query !== prevProps.query || this.props.tab !== prevProps.tab) {
|
|
this.setState({loading: true, referrers: null})
|
|
this.fetchReferrers()
|
|
}
|
|
}
|
|
|
|
showNoRef() {
|
|
return this.props.query.period === 'realtime'
|
|
}
|
|
|
|
fetchReferrers() {
|
|
const endpoint = UTM_TAGS[this.props.tab].endpoint
|
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/${endpoint}`, this.props.query, {show_noref: this.showNoRef()})
|
|
.then((res) => this.setState({loading: false, referrers: res}))
|
|
}
|
|
|
|
renderReferrer(referrer) {
|
|
const query = new URLSearchParams(window.location.search)
|
|
query.set(this.props.tab, referrer.name)
|
|
|
|
return (
|
|
<div
|
|
className="flex items-center justify-between my-1 text-sm"
|
|
key={referrer.name}
|
|
>
|
|
<Bar
|
|
count={referrer.count}
|
|
all={this.state.referrers}
|
|
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
|
maxWidthDeduction="4rem"
|
|
>
|
|
|
|
<span className="flex px-2 py-1.5 dark:text-gray-300 relative z-9 break-all">
|
|
<Link
|
|
className="md:truncate block hover:underline"
|
|
to={{search: query.toString()}}
|
|
>
|
|
{ referrer.name }
|
|
</Link>
|
|
</span>
|
|
</Bar>
|
|
<span className="font-medium dark:text-gray-200">{numberFormatter(referrer.count)}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
label() {
|
|
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
|
|
}
|
|
|
|
renderList() {
|
|
if (this.state.referrers && this.state.referrers.length > 0) {
|
|
return (
|
|
<div className="flex flex-col flex-grow">
|
|
<div className="flex items-center justify-between mt-3 mb-2 text-xs font-bold tracking-wide text-gray-500 dark:text-gray-400">
|
|
<span>{UTM_TAGS[this.props.tab].label}</span>
|
|
<span>{this.label()}</span>
|
|
</div>
|
|
|
|
<FlipMove className="flex-grow">
|
|
{this.state.referrers.map(this.renderReferrer.bind(this))}
|
|
</FlipMove>
|
|
<MoreLink site={this.props.site} list={this.state.referrers} endpoint={UTM_TAGS[this.props.tab].endpoint} />
|
|
</div>
|
|
)
|
|
} else {
|
|
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
|
|
}
|
|
}
|
|
|
|
renderContent() {
|
|
return (
|
|
<React.Fragment>
|
|
<div className="flex justify-between w-full">
|
|
<h3 className="font-bold dark:text-gray-100">Top Sources</h3>
|
|
{ this.props.renderTabs() }
|
|
</div>
|
|
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
|
|
<FadeIn show={!this.state.loading} className="flex flex-col flex-grow">
|
|
{ this.renderList() }
|
|
</FadeIn>
|
|
</React.Fragment>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
return (
|
|
<div
|
|
className="relative p-4 bg-white rounded shadow-xl stats-item flex flex-col dark:bg-gray-825 mt-6 w-full"
|
|
>
|
|
{ this.renderContent() }
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
export default class SourceList extends React.Component {
|
|
constructor(props) {
|
|
super(props)
|
|
this.tabKey = 'sourceTab__' + props.site.domain
|
|
const storedTab = storage.getItem(this.tabKey)
|
|
this.state = {
|
|
tab: storedTab || 'all'
|
|
}
|
|
}
|
|
|
|
setTab(tab) {
|
|
return () => {
|
|
storage.setItem(this.tabKey, tab)
|
|
this.setState({tab})
|
|
}
|
|
}
|
|
|
|
renderTabs() {
|
|
const activeClass = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold border-b-2 border-indigo-700 dark:border-indigo-500'
|
|
const defaultClass = 'hover:text-indigo-600 cursor-pointer'
|
|
return (
|
|
<ul className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
|
<li className={this.state.tab === 'all' ? activeClass : defaultClass} onClick={this.setTab('all')}>All</li>
|
|
<li className={this.state.tab === 'utm_medium' ? activeClass : defaultClass} onClick={this.setTab('utm_medium')}>Medium</li>
|
|
<li className={this.state.tab === 'utm_source' ? activeClass : defaultClass} onClick={this.setTab('utm_source')}>Source</li>
|
|
<li className={this.state.tab === 'utm_campaign' ? activeClass : defaultClass} onClick={this.setTab('utm_campaign')}>Campaign</li>
|
|
</ul>
|
|
)
|
|
}
|
|
|
|
render() {
|
|
if (this.state.tab === 'all') {
|
|
return <AllSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
|
} else if (this.state.tab === 'utm_medium') {
|
|
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
|
} else if (this.state.tab === 'utm_source') {
|
|
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
|
} else if (this.state.tab === 'utm_campaign') {
|
|
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
|
}
|
|
}
|
|
}
|