mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 09:33:19 +03:00
Refactor: Use ListReport component in Sources (#3153)
* refactor SourceList to use ListReport * refactor SourceList into a fn comp * change referrer-drilldown API response format and remove dead code * use ListReport in referrer-list * fix CI * fix flaky test * remove IO.inspect
This commit is contained in:
parent
dfa5bbf4a0
commit
b9d122c0c7
@ -19,7 +19,7 @@ class ReferrerDrilldownModal extends React.Component {
|
||||
const detailed = this.showExtra()
|
||||
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/${this.props.match.params.referrer}`, this.state.query, {limit: 100, detailed})
|
||||
.then((res) => this.setState({loading: false, referrers: res.referrers, totalVisitors: res.total_visitors}))
|
||||
.then((referrers) => this.setState({loading: false, referrers: referrers}))
|
||||
}
|
||||
|
||||
showExtra() {
|
||||
|
@ -7,6 +7,7 @@ import FadeIn from '../../fade-in'
|
||||
import MoreLink from '../more-link'
|
||||
import Bar from '../bar'
|
||||
import LazyLoader from '../../components/lazy-loader'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const MAX_ITEMS = 9
|
||||
const MIN_HEIGHT = 380
|
||||
@ -15,10 +16,29 @@ const ROW_GAP_HEIGHT = 4
|
||||
const DATA_CONTAINER_HEIGHT = (ROW_HEIGHT + ROW_GAP_HEIGHT) * (MAX_ITEMS - 1) + ROW_HEIGHT
|
||||
const COL_MIN_WIDTH = 70
|
||||
|
||||
function ExternalLink({item, externalLinkDest}) {
|
||||
if (externalLinkDest) {
|
||||
const dest = externalLinkDest(item)
|
||||
function FilterLink({filterQuery, onClick, children}) {
|
||||
const className = classNames('max-w-max w-full flex md:overflow-hidden', {
|
||||
'hover:underline': !!filterQuery
|
||||
})
|
||||
|
||||
if (filterQuery) {
|
||||
return (
|
||||
<Link
|
||||
to={{search: filterQuery.toString()}}
|
||||
onClick={onClick}
|
||||
className={className}
|
||||
>
|
||||
{ children }
|
||||
</Link>
|
||||
)
|
||||
} else {
|
||||
return <span className={className}>{ children }</span>
|
||||
}
|
||||
}
|
||||
|
||||
function ExternalLink({item, externalLinkDest}) {
|
||||
const dest = externalLinkDest && externalLinkDest(item)
|
||||
if (dest) {
|
||||
return (
|
||||
<a
|
||||
target="_blank"
|
||||
@ -63,7 +83,8 @@ function ExternalLink({item, externalLinkDest}) {
|
||||
// in realtime or goal-filtered views, we'll use `realtimeLabel` and `GoalFilterLabel`.
|
||||
|
||||
// * `getFilterFor` - a function that takes a list item and returns the query link (with
|
||||
// the filter) to navigate to when this list item is clicked.
|
||||
// the filter) to navigate to when this list item is clicked. If a list item is not
|
||||
// supposed to be clickable, this function should return `null` for that list item.
|
||||
|
||||
// OPTIONAL PROPS:
|
||||
|
||||
@ -76,8 +97,8 @@ function ExternalLink({item, externalLinkDest}) {
|
||||
// to navigate to. If this prop is given, an additional icon is rendered upon hovering
|
||||
// the entry.
|
||||
|
||||
// * `renderIconFor` - a function that takes a list item and returns the
|
||||
// HTML of an icon (such as a flag or screen size icon) for a listItem.
|
||||
// * `renderIcon` - a function that takes a list item and returns the
|
||||
// HTML of an icon (such as a flag, favicon, or a screen size icon) for a listItem.
|
||||
|
||||
// * `color` - color of the comparison bars in light-mode
|
||||
|
||||
@ -168,14 +189,19 @@ export default function ListReport(props) {
|
||||
)
|
||||
}
|
||||
|
||||
function renderBarFor(listItem) {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
function getFilterQuery(listItem) {
|
||||
const filter = props.getFilterFor(listItem)
|
||||
if (!filter) { return null }
|
||||
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
Object.entries(filter).forEach((([key, value]) => {
|
||||
query.set(key, value)
|
||||
}))
|
||||
|
||||
return query
|
||||
}
|
||||
|
||||
function renderBarFor(listItem) {
|
||||
const lightBackground = props.color || 'bg-green-50'
|
||||
const noop = () => {}
|
||||
const metricToPlot = metrics[0].name
|
||||
@ -189,13 +215,13 @@ export default function ListReport(props) {
|
||||
plot={metricToPlot}
|
||||
>
|
||||
<div className="flex justify-start px-2 py-1.5 group text-sm dark:text-gray-300 relative z-9 break-all w-full">
|
||||
<Link onClick={props.onClick || noop} to={{search: query.toString()}} className="max-w-max w-full flex hover:underline md:overflow-hidden">
|
||||
<FilterLink filterQuery={getFilterQuery(listItem)} onClick={props.onClick || noop}>
|
||||
{maybeRenderIconFor(listItem)}
|
||||
|
||||
<span className="w-full md:truncate">
|
||||
{listItem.name}
|
||||
</span>
|
||||
</Link>
|
||||
</FilterLink>
|
||||
<ExternalLink item={listItem} externalLinkDest={props.externalLinkDest} />
|
||||
</div>
|
||||
</Bar>
|
||||
|
@ -1,177 +1,48 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
import FlipMove from 'react-flip-move';
|
||||
|
||||
import FadeIn from '../../fade-in'
|
||||
import Bar from '../bar'
|
||||
import MoreLink from '../more-link'
|
||||
import numberFormatter from '../../util/number-formatter'
|
||||
import * as api from '../../api'
|
||||
import LazyLoader from '../../components/lazy-loader'
|
||||
import * as url from '../../util/url'
|
||||
import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics'
|
||||
import ListReport from '../reports/list'
|
||||
|
||||
function LinkOption(props) {
|
||||
if (props.disabled) {
|
||||
return <span {...props}>{props.children}</span>
|
||||
} else {
|
||||
props = Object.assign({}, props, { className: props.className + ' hover:underline' })
|
||||
return <Link {...props}>{props.children}</Link>
|
||||
}
|
||||
}
|
||||
|
||||
export default class Referrers extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { loading: true }
|
||||
this.onVisible = this.onVisible.bind(this)
|
||||
this.fetchReferrers = this.fetchReferrers.bind(this)
|
||||
export default function Referrers({site, query}) {
|
||||
function fetchReferrers() {
|
||||
return api.get(url.apiPath(site, `/referrers/${encodeURIComponent(query.filters.source)}`), query, {limit: 9})
|
||||
}
|
||||
|
||||
onVisible() {
|
||||
this.fetchReferrers()
|
||||
if (this.props.query.period === 'realtime') {
|
||||
document.addEventListener('tick', this.fetchReferrers)
|
||||
}
|
||||
function externalLinkDest(referrer) {
|
||||
if (referrer.name === 'Direct / None') { return null }
|
||||
return `https://${referrer.name}`
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query) {
|
||||
this.setState({ loading: true, referrers: null })
|
||||
this.fetchReferrers()
|
||||
}
|
||||
function getFilterFor(referrer) {
|
||||
if (referrer.name === 'Direct / None') { return null }
|
||||
return { referrer: referrer.name }
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('tick', this.fetchReferrers)
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchReferrers() {
|
||||
if (this.props.query.filters.source) {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/${encodeURIComponent(this.props.query.filters.source)}`, this.props.query)
|
||||
.then((res) => res.search_terms || res.referrers)
|
||||
.then((referrers) => this.setState({ loading: false, referrers: referrers }))
|
||||
} else if (this.props.query.filters.goal) {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/goal/referrers`, this.props.query)
|
||||
.then((res) => this.setState({ loading: false, referrers: res }))
|
||||
} else {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers`, this.props.query)
|
||||
.then((res) => this.setState({ loading: false, referrers: res }))
|
||||
}
|
||||
}
|
||||
|
||||
renderExternalLink(referrer) {
|
||||
if (this.props.query.filters.source && this.props.query.filters.source !== 'Google' && referrer.name !== 'Direct / None') {
|
||||
function renderIcon(listItem) {
|
||||
return (
|
||||
<a target="_blank" href={'//' + referrer.name} rel="noreferrer" className="hidden group-hover:block">
|
||||
<svg className="inline w-4 h-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
|
||||
}
|
||||
|
||||
renderReferrer(referrer) {
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
query.set('referrer', referrer.name)
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between my-1 text-sm" key={referrer.name}>
|
||||
<Bar
|
||||
count={referrer.visitors}
|
||||
all={this.state.referrers}
|
||||
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<span className="flex px-2 py-1.5 z-9 relative break-all group">
|
||||
<LinkOption
|
||||
className="block md:truncate dark:text-gray-300"
|
||||
to={{ search: query.toString() }}
|
||||
disabled={referrer.name === 'Direct / None'}
|
||||
>
|
||||
<img
|
||||
src={`/favicon/sources/${encodeURIComponent(referrer.name)}`}
|
||||
src={`/favicon/sources/${encodeURIComponent(listItem.name)}`}
|
||||
referrerPolicy="no-referrer"
|
||||
className="inline w-4 h-4 mr-2 -mt-px align-middle"
|
||||
/>
|
||||
{referrer.name}
|
||||
</LinkOption>
|
||||
{this.renderExternalLink(referrer)}
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200">{numberFormatter(referrer.visitors)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
if (this.props.query.period === 'realtime') {
|
||||
return 'Current visitors'
|
||||
}
|
||||
|
||||
if (this.showConversionRate()) {
|
||||
return 'Conversions'
|
||||
}
|
||||
|
||||
return 'Visitors'
|
||||
}
|
||||
|
||||
renderList() {
|
||||
if (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"
|
||||
>
|
||||
<span>Referrer</span>
|
||||
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{this.label()}</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlipMove className="flex-grow">
|
||||
{this.state.referrers.map(this.renderReferrer.bind(this))}
|
||||
</FlipMove>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400"
|
||||
>
|
||||
No data yet
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.state.referrers) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderList()}
|
||||
<MoreLink site={this.props.site} list={this.state.referrers} endpoint={`referrers/${this.props.query.filters.source}`} className="w-full pb-4 absolute bottom-0 left-0"/>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="flex flex-col flex-grow">
|
||||
<LazyLoader onVisible={this.onVisible}>
|
||||
<h3 className="font-bold dark:text-gray-100">Top Referrers</h3>
|
||||
{this.state.loading && <div className="mx-auto loading mt-44"><div></div></div>}
|
||||
<FadeIn show={!this.state.loading}>
|
||||
{this.renderContent()}
|
||||
</FadeIn>
|
||||
</LazyLoader>
|
||||
<ListReport
|
||||
fetchData={fetchReferrers}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Referrer"
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
detailsLink={url.sitePath(site, `/referrers/${encodeURIComponent(query.filters.source)}`)}
|
||||
query={query}
|
||||
externalLinkDest={externalLinkDest}
|
||||
renderIcon={renderIcon}
|
||||
color="bg-blue-50"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,304 +1,108 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom'
|
||||
import FlipMove from 'react-flip-move';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
|
||||
import * as storage from '../../util/storage'
|
||||
import FadeIn from '../../fade-in'
|
||||
import Bar from '../bar'
|
||||
import MoreLink from '../more-link'
|
||||
import numberFormatter from '../../util/number-formatter'
|
||||
import * as api from '../../api'
|
||||
import * as url from '../../util/url'
|
||||
import LazyLoader from '../../components/lazy-loader'
|
||||
|
||||
class AllSources extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.onVisible = this.onVisible.bind(this)
|
||||
this.fetchReferrers = this.fetchReferrers.bind(this)
|
||||
this.state = { loading: true }
|
||||
}
|
||||
|
||||
onVisible() {
|
||||
this.fetchReferrers()
|
||||
if (this.props.query.period === 'realtime') {
|
||||
document.addEventListener('tick', this.fetchReferrers)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query) {
|
||||
this.setState({ loading: true, referrers: null })
|
||||
this.fetchReferrers()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('tick', this.fetchReferrers)
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchReferrers() {
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/sources`, this.props.query)
|
||||
.then((res) => this.setState({ loading: false, referrers: res }))
|
||||
}
|
||||
|
||||
renderReferrer(referrer) {
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between my-1 text-sm"
|
||||
key={referrer.name}
|
||||
>
|
||||
<Bar
|
||||
count={referrer.visitors}
|
||||
all={this.state.referrers}
|
||||
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
<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={url.setQuery('source', referrer.name)}
|
||||
>
|
||||
<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 w-20 text-right" tooltip={referrer.visitors}>{numberFormatter(referrer.visitors)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
if (this.props.query.period === 'realtime') {
|
||||
return 'Current visitors'
|
||||
}
|
||||
|
||||
if (this.showConversionRate()) {
|
||||
return 'Conversions'
|
||||
}
|
||||
|
||||
return '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>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{this.label()}</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</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" className="pb-4 absolute bottom-0 left-0"/>
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="flex flex-col flex-grow">
|
||||
<LazyLoader 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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const UTM_TAGS = {
|
||||
utm_medium: { label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: 'utm_mediums' },
|
||||
utm_source: { label: 'UTM Source', shortLabel: 'UTM Source', endpoint: 'utm_sources' },
|
||||
utm_campaign: { label: 'UTM Campaign', shortLabel: 'UTM Campai', endpoint: 'utm_campaigns' },
|
||||
utm_content: { label: 'UTM Content', shortLabel: 'UTM Conten', endpoint: 'utm_contents' },
|
||||
utm_term: { label: 'UTM Term', shortLabel: 'UTM Term', endpoint: 'utm_terms' },
|
||||
}
|
||||
|
||||
class UTMSources extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.onVisible = this.onVisible.bind(this)
|
||||
this.fetchReferrers = this.fetchReferrers.bind(this)
|
||||
this.state = { loading: true }
|
||||
}
|
||||
|
||||
onVisible() {
|
||||
this.fetchReferrers()
|
||||
if (this.props.query.period === 'realtime') {
|
||||
document.addEventListener('tick', this.fetchReferrers)
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.query !== prevProps.query || this.props.tab !== prevProps.tab) {
|
||||
this.setState({ loading: true, referrers: null })
|
||||
this.fetchReferrers()
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('tick', this.fetchReferrers)
|
||||
}
|
||||
|
||||
showNoRef() {
|
||||
return this.props.query.period === 'realtime'
|
||||
}
|
||||
|
||||
showConversionRate() {
|
||||
return !!this.props.query.filters.goal
|
||||
}
|
||||
|
||||
fetchReferrers() {
|
||||
const endpoint = UTM_TAGS[this.props.tab].endpoint
|
||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/${endpoint}`, this.props.query)
|
||||
.then((res) => this.setState({ loading: false, referrers: res }))
|
||||
}
|
||||
|
||||
renderReferrer(referrer) {
|
||||
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex items-center justify-between my-1 text-sm"
|
||||
key={referrer.name}
|
||||
>
|
||||
<Bar
|
||||
count={referrer.visitors}
|
||||
all={this.state.referrers}
|
||||
bg="bg-blue-50 dark:bg-gray-500 dark:bg-opacity-15"
|
||||
maxWidthDeduction={maxWidthDeduction}
|
||||
>
|
||||
|
||||
<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={url.setQuery(this.props.tab, referrer.name)}
|
||||
>
|
||||
{referrer.name}
|
||||
</Link>
|
||||
</span>
|
||||
</Bar>
|
||||
<span className="font-medium dark:text-gray-200 w-20 text-right" tooltip={referrer.visitors}>{numberFormatter(referrer.visitors)}</span>
|
||||
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{referrer.conversion_rate}%</span>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
label() {
|
||||
if (this.props.query.period === 'realtime') {
|
||||
return 'Current visitors'
|
||||
}
|
||||
|
||||
if (this.showConversionRate()) {
|
||||
return 'Conversions'
|
||||
}
|
||||
|
||||
return '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>
|
||||
<div className="text-right">
|
||||
<span className="inline-block w-20">{this.label()}</span>
|
||||
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
|
||||
</div>
|
||||
</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} className="pb-4 absolute bottom-0 left-0"/>
|
||||
</div>
|
||||
)
|
||||
} else {
|
||||
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<LazyLoader onVisible={this.onVisible}>
|
||||
<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>
|
||||
</LazyLoader>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import { Fragment } from 'react'
|
||||
import * as api from '../../api'
|
||||
import ListReport from '../reports/list'
|
||||
import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics';
|
||||
import { Menu, Transition } from '@headlessui/react'
|
||||
import { ChevronDownIcon } from '@heroicons/react/20/solid'
|
||||
import classNames from 'classnames'
|
||||
|
||||
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'
|
||||
}
|
||||
const UTM_TAGS = {
|
||||
utm_medium: { label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: '/utm_mediums' },
|
||||
utm_source: { label: 'UTM Source', shortLabel: 'UTM Source', endpoint: '/utm_sources' },
|
||||
utm_campaign: { label: 'UTM Campaign', shortLabel: 'UTM Campai', endpoint: '/utm_campaigns' },
|
||||
utm_content: { label: 'UTM Content', shortLabel: 'UTM Conten', endpoint: '/utm_contents' },
|
||||
utm_term: { label: 'UTM Term', shortLabel: 'UTM Term', endpoint: '/utm_terms' },
|
||||
}
|
||||
|
||||
function AllSources(props) {
|
||||
const {site, query} = props
|
||||
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, '/sources'), query, {limit: 9})
|
||||
}
|
||||
|
||||
setTab(tab) {
|
||||
function getFilterFor(listItem) {
|
||||
return { source: listItem['name']}
|
||||
}
|
||||
|
||||
function renderIcon(listItem) {
|
||||
return (
|
||||
<img
|
||||
src={`/favicon/sources/${encodeURIComponent(listItem.name)}`}
|
||||
className="inline w-4 h-4 mr-2 -mt-px align-middle"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel="Source"
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
detailsLink={url.sitePath(site, '/sources')}
|
||||
renderIcon={renderIcon}
|
||||
query={query}
|
||||
color="bg-blue-50"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function UTMSources(props) {
|
||||
const {site, query} = props
|
||||
const utmTag = UTM_TAGS[props.tab]
|
||||
|
||||
function fetchData() {
|
||||
return api.get(url.apiPath(site, utmTag.endpoint), query, {limit: 9})
|
||||
}
|
||||
|
||||
function getFilterFor(listItem) {
|
||||
return { exit_page: listItem['name']}
|
||||
}
|
||||
|
||||
return (
|
||||
<ListReport
|
||||
fetchData={fetchData}
|
||||
getFilterFor={getFilterFor}
|
||||
keyLabel={utmTag.label}
|
||||
metrics={maybeWithCR([VISITORS_METRIC], query)}
|
||||
detailsLink={url.sitePath(site, utmTag.endpoint)}
|
||||
query={query}
|
||||
color="bg-blue-50"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SourceList(props) {
|
||||
const {site, query} = props
|
||||
const tabKey = 'sourceTab__' + props.site.domain
|
||||
const storedTab = storage.getItem(tabKey)
|
||||
const [currentTab, setCurrentTab] = useState(storedTab || 'all')
|
||||
|
||||
function setTab(tab) {
|
||||
return () => {
|
||||
storage.setItem(this.tabKey, tab)
|
||||
this.setState({ tab })
|
||||
storage.setItem(tabKey, tab)
|
||||
setCurrentTab(tab)
|
||||
}
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
function renderTabs() {
|
||||
const activeClass = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold active-prop-heading truncate text-left'
|
||||
const defaultClass = 'hover:text-indigo-600 cursor-pointer truncate text-left'
|
||||
const dropdownOptions = Object.keys(UTM_TAGS)
|
||||
let buttonText = UTM_TAGS[this.state.tab] ? UTM_TAGS[this.state.tab].label : 'Campaigns'
|
||||
let buttonText = UTM_TAGS[currentTab] ? UTM_TAGS[currentTab].label : 'Campaigns'
|
||||
|
||||
return (
|
||||
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
|
||||
<div className={this.state.tab === 'all' ? activeClass : defaultClass} onClick={this.setTab('all')}>All</div>
|
||||
<div className={currentTab === 'all' ? activeClass : defaultClass} onClick={setTab('all')}>All</div>
|
||||
|
||||
<Menu as="div" className="relative inline-block text-left">
|
||||
<div>
|
||||
<Menu.Button className="inline-flex justify-between focus:outline-none">
|
||||
<span className={this.state.tab.startsWith('utm_') ? activeClass : defaultClass}>{buttonText}</span>
|
||||
<span className={currentTab.startsWith('utm_') ? activeClass : defaultClass}>{buttonText}</span>
|
||||
<ChevronDownIcon className="-mr-1 ml-1 h-4 w-4" aria-hidden="true" />
|
||||
</Menu.Button>
|
||||
</div>
|
||||
@ -319,11 +123,11 @@ export default class SourceList extends React.Component {
|
||||
<Menu.Item key={option}>
|
||||
{({ active }) => (
|
||||
<span
|
||||
onClick={this.setTab(option)}
|
||||
onClick={setTab(option)}
|
||||
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',
|
||||
this.state.tab === option ? 'font-bold' : ''
|
||||
currentTab === option ? 'font-bold' : ''
|
||||
)}
|
||||
>
|
||||
{UTM_TAGS[option].label}
|
||||
@ -340,11 +144,25 @@ export default class SourceList extends React.Component {
|
||||
)
|
||||
}
|
||||
|
||||
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 (Object.keys(UTM_TAGS).includes(this.state.tab)) {
|
||||
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
||||
function renderContent() {
|
||||
if (currentTab === 'all') {
|
||||
return <AllSources site={site} query={query} />
|
||||
} else {
|
||||
return <UTMSources tab={currentTab} site={site} query={query} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Header Container */}
|
||||
<div className="w-full flex justify-between">
|
||||
<h3 className="font-bold dark:text-gray-100">
|
||||
Top Sources
|
||||
</h3>
|
||||
{ renderTabs() }
|
||||
</div>
|
||||
{/* Main Contents */}
|
||||
{ renderContent() }
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -754,10 +754,8 @@ defmodule PlausibleWeb.Api.StatsController do
|
||||
Stats.breakdown(site, query, "visit:referrer", metrics, pagination)
|
||||
|> add_cr(site, query, pagination, :referrer, "visit:referrer")
|
||||
|> transform_keys(%{referrer: :name})
|
||||
|> Enum.map(&Map.drop(&1, [:visits]))
|
||||
|
||||
%{:visitors => %{value: total_visitors}} = Stats.aggregate(site, query, [:visitors])
|
||||
json(conn, %{referrers: referrers, total_visitors: total_visitors})
|
||||
json(conn, referrers)
|
||||
end
|
||||
|
||||
def pages(conn, params) do
|
||||
|
@ -47,7 +47,8 @@ defmodule PlausibleWeb.Api.InternalControllerTest do
|
||||
|
||||
%{"data" => sites} = json_response(conn, 200)
|
||||
|
||||
assert Enum.map(sites, & &1["domain"]) == [site.domain, site2.domain]
|
||||
assert %{"domain" => site.domain} in sites
|
||||
assert %{"domain" => site2.domain} in sites
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -923,13 +923,10 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"total_visitors" => 3,
|
||||
"referrers" => [
|
||||
assert json_response(conn, 200) == [
|
||||
%{"name" => "10words.com", "visitors" => 2},
|
||||
%{"name" => "10words.com/page1", "visitors" => 1}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
test "calculates bounce rate and visit duration for referrer urls", %{conn: conn, site: site} do
|
||||
@ -964,9 +961,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day&date=2021-01-01&detailed=true"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"total_visitors" => 2,
|
||||
"referrers" => [
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"name" => "10words.com",
|
||||
"visitors" => 2,
|
||||
@ -974,7 +969,6 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"visit_duration" => 450
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
test "gets keywords from Google", %{conn: conn, user: user, site: site} do
|
||||
@ -1032,9 +1026,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"total_visitors" => 1,
|
||||
"referrers" => [
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"name" => "10words.com",
|
||||
"total_visitors" => 2,
|
||||
@ -1042,7 +1034,6 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"visitors" => 1
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
test "returns top referring urls for a pageview goal", %{conn: conn, site: site} do
|
||||
@ -1073,9 +1064,7 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"/api/stats/#{site.domain}/referrers/10words?period=day&filters=#{filters}"
|
||||
)
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"total_visitors" => 1,
|
||||
"referrers" => [
|
||||
assert json_response(conn, 200) == [
|
||||
%{
|
||||
"name" => "10words.com",
|
||||
"total_visitors" => 2,
|
||||
@ -1083,7 +1072,6 @@ defmodule PlausibleWeb.Api.StatsController.SourcesTest do
|
||||
"visitors" => 1
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user