JS refactor: use generic ListReport for country report (#1487)

* Use ListReport for countries

* Fix countries tests

* Replace Browsers with ListReport

* Use Listreport for OS and screen size
This commit is contained in:
Uku Taht 2021-11-25 12:00:17 +02:00 committed by GitHub
parent dae99a94b2
commit 2bdfec1cc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 166 additions and 616 deletions

View File

@ -2,7 +2,6 @@ import React, { Fragment, useState } from 'react';
import { Link, withRouter } from 'react-router-dom'
import { AdjustmentsIcon, PlusIcon, XIcon, PencilIcon } from '@heroicons/react/solid'
import classNames from 'classnames'
import Datamap from 'datamaps'
import { Menu, Transition } from '@headlessui/react'
import { appliedFilters, navigateToQuery, formattedFilters } from './query'
@ -13,8 +12,10 @@ function removeFilter(key, history, query) {
[key]: false
}
if (key === 'goal') { newOpts.props = false }
if (key === 'country') { newOpts.country_name = false }
if (key === 'region') { newOpts.region_name = false }
if (key === 'city') { newOpts.city_name = false }
navigateToQuery(
history,
query,
@ -59,9 +60,9 @@ function filterText(key, rawValue, query) {
return <>{osName}.Version {type} <b>{value}</b></>
}
if (key === "country") {
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
const selectedCountry = allCountries.find((c) => c.id === value) || {properties: {name: value}};
return <>Country {type} <b>{selectedCountry.properties.name}</b></>
const q = new URLSearchParams(window.location.search)
const countryName = q.get('country_name')
return <>Country {type} <b>{countryName}</b></>
}
if (key === "region") {

View File

@ -8,7 +8,6 @@ import GoogleKeywordsModal from './stats/modals/google-keywords'
import PagesModal from './stats/modals/pages'
import EntryPagesModal from './stats/modals/entry-pages'
import ExitPagesModal from './stats/modals/exit-pages'
import CountriesModal from './stats/modals/countries'
import ModalTable from './stats/modals/table'
import FilterModal from './stats/modals/filter'
@ -50,7 +49,7 @@ export default function Router({site, loggedIn, currentUserRole}) {
<ExitPagesModal site={site} />
</Route>
<Route path="/:domain/countries">
<CountriesModal site={site} />
<ModalTable title="Top countries" site={site} endpoint={`/api/stats/${encodeURIComponent(site.domain)}/countries`} filter={{country: 'code', country_name: 'name'}} keyLabel="Country" />
</Route>
<Route path="/:domain/regions">
<ModalTable title="Top regions" site={site} endpoint={`/api/stats/${encodeURIComponent(site.domain)}/regions`} filter={{region: 'code', region_name: 'name'}} keyLabel="Region" />

View File

@ -1,117 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom'
import FadeIn from '../../fade-in'
import numberFormatter from '../../number-formatter'
import Bar from '../bar'
import * as api from '../../api'
import * as url from '../../url'
import LazyLoader from '../../lazy-loader'
export default class Browsers extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
this.renderBrowserContent = this.renderBrowserContent.bind(this)
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({loading: true, browsers: null})
this.fetchBrowsers()
}
}
onVisible() {
this.fetchBrowsers()
if (this.props.timer) this.props.timer.onTick(this.fetchBrowsers.bind(this))
}
fetchBrowsers() {
if (this.props.query.filters.browser) {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/browser-versions`, this.props.query)
.then((res) => this.setState({loading: false, browsers: res}))
} else {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/browsers`, this.props.query)
.then((res) => this.setState({loading: false, browsers: res}))
}
}
showConversionRate() {
return !!this.props.query.filters.goal
}
label() {
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
}
renderBrowserContent(browser, link) {
return (
<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={link}>
{browser.name}
</Link>
</span>
)
}
renderBrowser(browser) {
let link;
if (this.props.query.filters.browser) {
link = url.setQuery('browser_version', browser.name)
} else {
link = url.setQuery('browser', browser.name)
}
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
return (
<div className="flex items-center justify-between my-1 text-sm" key={browser.name}>
<Bar
count={browser.visitors}
all={this.state.browsers}
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
maxWidthDeduction={maxWidthDeduction}
>
{this.renderBrowserContent(browser, link)}
</Bar>
<span className="font-medium dark:text-gray-200 text-right w-20">
{numberFormatter(browser.visitors)} <span className="inline-block w-8 text-xs"> ({browser.percentage}%)</span>
</span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(browser.conversion_rate)}%</span>}
</div>
)
}
renderList() {
const key = this.props.query.filters.browser ? this.props.query.filters.browser + ' version' : 'Browser'
if (this.state.browsers && this.state.browsers.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 dark:text-gray-400">
<span>{ key }</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>
{ this.state.browsers && this.state.browsers.map(this.renderBrowser.bind(this)) }
</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 (
<LazyLoader onVisible={this.onVisible} className="flex flex-col flex-grow">
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
<FadeIn show={!this.state.loading} className="flex-grow">
{ this.renderList() }
</FadeIn>
</LazyLoader>
)
}
}

View File

@ -1,16 +1,94 @@
import React from 'react';
import { Link } from 'react-router-dom'
import * as storage from '../../storage'
import LazyLoader from '../../lazy-loader'
import Browsers from './browsers'
import OperatingSystems from './operating-systems'
import FadeIn from '../../fade-in'
import numberFormatter from '../../number-formatter'
import Bar from '../bar'
import ListReport from '../reports/list'
import * as api from '../../api'
import * as url from '../../url'
function Browsers({query, site}) {
function fetchData() {
return api.get(url.apiPath(site, '/browsers'), query)
}
return (
<ListReport
fetchData={fetchData}
filter={{browser: 'name'}}
keyLabel="Browser"
query={query}
/>
)
}
function BrowserVersions({query, site}) {
function fetchData() {
return api.get(url.apiPath(site, '/browser-versions'), query)
}
return (
<ListReport
fetchData={fetchData}
filter={{browser_version: 'name'}}
keyLabel={query.filters.browser + ' version'}
query={query}
/>
)
}
function OperatingSystems({query, site}) {
function fetchData() {
return api.get(url.apiPath(site, '/operating-systems'), query)
}
return (
<ListReport
fetchData={fetchData}
filter={{os: 'name'}}
keyLabel="Operating system"
query={query}
/>
)
}
function OperatingSystemVersions({query, site}) {
function fetchData() {
return api.get(url.apiPath(site, '/operating-system-versions'), query)
}
return (
<ListReport
fetchData={fetchData}
filter={{os_version: 'name'}}
keyLabel={query.filters.os + ' version'}
query={query}
/>
)
}
function ScreenSizes({query, site}) {
function fetchData() {
return api.get(url.apiPath(site, '/screen-sizes'), query)
}
function renderIcon(screenSize) {
return iconFor(screenSize.name)
}
function renderTooltipText(screenSize) {
return EXPLANATION[screenSize.name]
}
return (
<ListReport
fetchData={fetchData}
filter={{screen: 'name'}}
keyLabel="Screen size"
query={query}
renderIcon={renderIcon}
tooltipText={renderTooltipText}
/>
)
}
const EXPLANATION = {
'Mobile': 'up to 576px',
@ -39,110 +117,6 @@ function iconFor(screenSize) {
}
}
class ScreenSizes extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({loading: true, sizes: null})
this.fetchScreenSizes()
}
}
onVisible() {
this.fetchScreenSizes()
if (this.props.timer) this.props.timer.onTick(this.fetchScreenSizes.bind(this))
}
fetchScreenSizes() {
api.get(
`/api/stats/${encodeURIComponent(this.props.site.domain)}/screen-sizes`,
this.props.query
)
.then((res) => this.setState({loading: false, sizes: res}))
}
showConversionRate() {
return !!this.props.query.filters.goal
}
label() {
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
}
renderScreenSize(size) {
const query = new URLSearchParams(window.location.search)
query.set('screen', size.name)
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
return (
<div className="flex items-center justify-between my-1 text-sm" key={size.name}>
<Bar
count={size.visitors}
all={this.state.sizes}
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
maxWidthDeduction={maxWidthDeduction}
>
<span
tooltip={EXPLANATION[size.name]}
className="flex px-2 py-1.5 dark:text-gray-300"
>
<Link className="md:truncate block hover:underline" to={url.setQuery('screen', size.name)}>
{iconFor(size.name)} {size.name}
</Link>
</span>
</Bar>
<span className="font-medium dark:text-gray-200 text-right w-20">
{numberFormatter(size.visitors)} <span className="inline-block w-8 text-xs text-right">({size.percentage}%)</span>
</span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(size.conversion_rate)}%</span>}
</div>
)
}
renderList() {
if (this.state.sizes && this.state.sizes.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>Screen size</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>
{ this.state.sizes && this.state.sizes.map(this.renderScreenSize.bind(this)) }
</React.Fragment>
)
}
return (
<div
className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400"
>
No data yet
</div>
)
}
render() {
return (
<LazyLoader onVisible={this.onVisible} className="flex flex-col flex-grow">
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
<FadeIn show={!this.state.loading} class="flex-grow">
{ this.renderList() }
</FadeIn>
</LazyLoader>
)
}
}
export default class Devices extends React.Component {
constructor(props) {
super(props)
@ -164,23 +138,19 @@ export default class Devices extends React.Component {
renderContent() {
switch (this.state.mode) {
case 'browser':
if (this.props.query.filters.browser) {
return <BrowserVersions site={this.props.site} query={this.props.query} timer={this.props.timer} />
}
return <Browsers site={this.props.site} query={this.props.query} timer={this.props.timer} />
case 'os':
return (
<OperatingSystems
site={this.props.site}
query={this.props.query}
timer={this.props.timer}
/>
)
if (this.props.query.filters.os) {
return <OperatingSystemVersions site={this.props.site} query={this.props.query} timer={this.props.timer} />
}
return <OperatingSystems site={this.props.site} query={this.props.query} timer={this.props.timer} />
case 'size':
default:
return (
<ScreenSizes
site={this.props.site}
query={this.props.query}
timer={this.props.timer}
/>
<ScreenSizes site={this.props.site} query={this.props.query} timer={this.props.timer} />
)
}
}

View File

@ -1,111 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom'
import FadeIn from '../../fade-in'
import numberFormatter from '../../number-formatter'
import Bar from '../bar'
import * as api from '../../api'
import * as url from '../../url'
import LazyLoader from '../../lazy-loader'
export default class OperatingSystems extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
}
onVisible() {
this.fetchOperatingSystems()
if (this.props.timer) this.props.timer.onTick(this.fetchOperatingSystems.bind(this))
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({loading: true, operatingSystems: null})
this.fetchOperatingSystems()
}
}
fetchOperatingSystems() {
if (this.props.query.filters.os) {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/operating-system-versions`, this.props.query)
.then((res) => this.setState({loading: false, operatingSystems: res}))
} else {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/operating-systems`, this.props.query)
.then((res) => this.setState({loading: false, operatingSystems: res}))
}
}
showConversionRate() {
return !!this.props.query.filters.goal
}
renderOperatingSystem(os) {
let link;
if (this.props.query.filters.os) {
link = url.setQuery('os_version', os.name)
} else {
link = url.setQuery('os', os.name)
}
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
return (
<div
className="flex items-center justify-between my-1 text-sm"
key={os.name}
>
<Bar
count={os.visitors}
all={this.state.operatingSystems}
bg="bg-green-50 dark: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={link}>
{os.name}
</Link>
</span>
</Bar>
<span className="font-medium dark:text-gray-200 text-right w-20">{numberFormatter(os.visitors)} <span className="inline-block w-8 text-xs text-right">({os.percentage}%)</span></span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(os.conversion_rate)}%</span>}
</div>
)
}
label() {
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
}
renderList() {
const key = this.props.query.filters.os ? this.props.query.filters.os + ' version' : 'Operating system'
if (this.state.operatingSystems && this.state.operatingSystems.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 dark:text-gray-400">
<span>{ key }</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>
{ this.state.operatingSystems && this.state.operatingSystems.map(this.renderOperatingSystem.bind(this)) }
</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 (
<LazyLoader onVisible={this.onVisible} className="flex flex-col flex-grow">
{ this.state.loading && <div className="mx-auto loading mt-44"><div></div></div> }
<FadeIn show={!this.state.loading} className="flex-grow">
{ this.renderList() }
</FadeIn>
</LazyLoader>
)
}
}

View File

@ -1,118 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom'
import FlipMove from 'react-flip-move';
import Datamap from 'datamaps'
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 * as url from '../../url'
import LazyLoader from '../../lazy-loader'
export default class Countries extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
this.onVisible = this.onVisible.bind(this)
}
componentDidUpdate(prevProps) {
if (this.props.query !== prevProps.query) {
this.setState({loading: true, countries: null})
this.fetchCountries()
}
}
onVisible() {
this.fetchCountries()
if (this.props.timer) this.props.timer.onTick(this.fetchCountries.bind(this))
}
showConversionRate() {
return !!this.props.query.filters.goal
}
fetchCountries() {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.props.query)
.then((res) => this.setState({loading: false, countries: res}))
}
label() {
if (this.showConversionRate()) {
return 'Conversions'
}
return 'Visitors'
}
renderCountry(country) {
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
const thisCountry = allCountries.find((c) => c.id === country.name) || {properties: {name: country.name}};
const countryFullName = thisCountry.properties.name
return (
<div
className="flex items-center justify-between my-1 text-sm"
key={country.name}
>
<Bar
count={country.visitors}
all={this.state.countries}
bg="bg-orange-50 dark:bg-gray-500 dark:bg-opacity-15"
maxWidthDeduction={maxWidthDeduction}
>
<span
className="flex px-2 py-1.5 group dark:text-gray-300 relative z-9 break-all"
>
<Link
to={url.setQuery('country', country.name)}
className="md:truncate block hover:underline"
>
{countryFullName}
</Link>
</span>
</Bar>
<span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(country.visitors)}</span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{numberFormatter(country.conversion_rate)}%</span>}
</div>
)
}
renderList() {
if (this.state.countries && this.state.countries.length > 0) {
return (
<>
<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>Country</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>
{ this.state.countries.map(this.renderCountry.bind(this)) }
</FlipMove>
</>
)
}
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
}
render() {
const { loading } = this.state;
return (
<LazyLoader onVisible={this.onVisible} className="flex flex-col flex-grow">
{ loading && <div className="mx-auto loading mt-44"><div></div></div> }
<FadeIn show={!loading} className="flex-grow">
{ this.renderList() }
</FadeIn>
{!loading && <MoreLink site={this.props.site} list={this.state.countries} endpoint="countries" />}
</LazyLoader>
)
}
}

View File

@ -1,13 +1,31 @@
import React from 'react';
import * as storage from '../../storage'
import Countries from './countries';
import CountriesMap from './map'
import * as api from '../../api'
import {apiPath, sitePath} from '../../url'
import ListReport from '../reports/list'
function Countries({query, site}) {
function fetchData() {
return api.get(apiPath(site, '/countries'), query, {limit: 9}).then((res) => {
return res.map(row => Object.assign({}, row, {percentage: undefined}))
})
}
return (
<ListReport
fetchData={fetchData}
filter={{country: 'code', country_name: 'name'}}
keyLabel="Country"
detailsLink={sitePath(site, '/countries')}
query={query}
color="bg-orange-50"
/>
)
}
function Regions({query, site}) {
function fetchData() {
return api.get(apiPath(site, '/regions'), query, {country_name: query.filters.country, limit: 9})
@ -15,12 +33,12 @@ function Regions({query, site}) {
return (
<ListReport
title="Regions"
fetchData={fetchData}
filter={{region: 'code', region_name: 'name'}}
keyLabel="Region"
detailsLink={sitePath(site, '/regions')}
query={query}
color="bg-orange-50"
/>
)
}
@ -32,12 +50,12 @@ function Cities({query, site}) {
return (
<ListReport
title="Cities"
fetchData={fetchData}
filter={{city: 'code', city_name: 'name'}}
keyLabel="City"
detailsLink={sitePath(site, '/cities')}
query={query}
color="bg-orange-50"
/>
)
}

View File

@ -1,119 +0,0 @@
import React from "react";
import { Link, withRouter } from 'react-router-dom'
import Datamap from 'datamaps'
import Modal from './modal'
import * as api from '../../api'
import numberFormatter from '../../number-formatter'
import {parseQuery} from '../../query'
class CountriesModal extends React.Component {
constructor(props) {
super(props)
this.state = {
loading: true,
query: parseQuery(props.location.search, props.site)
}
}
componentDidMount() {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/countries`, this.state.query, {limit: 300})
.then((res) => this.setState({loading: false, countries: res}))
}
label() {
if (this.state.query.period === 'realtime') {
return 'Current visitors'
}
if (this.showConversionRate()) {
return 'Conversions'
}
return 'Visitors'
}
showConversionRate() {
return !!this.state.query.filters.goal
}
renderCountry(country) {
const query = new URLSearchParams(window.location.search)
query.set('country', country.name)
const allCountries = Datamap.prototype.worldTopo.objects.world.geometries;
const thisCountry = allCountries.find((c) => c.id === country.name) || {properties: {name: country.name}};
const countryFullName = thisCountry.properties.name
return (
<tr className="text-sm dark:text-gray-200" key={country.name}>
<td className="p-2">
<Link className="hover:underline" to={{search: query.toString(), pathname: `/${encodeURIComponent(this.props.site.domain)}`}}>
{countryFullName}
</Link>
</td>
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{country.total_visitors}</td> }
<td className="p-2 w-32 font-medium" align="right">
{numberFormatter(country.visitors)} {!this.showConversionRate() && <span className="inline-block text-xs w-8 text-right">({country.percentage}%)</span>}
</td>
{this.showConversionRate() && <td className="p-2 w-32 font-medium" align="right">{country.conversion_rate}%</td> }
</tr>
)
}
renderBody() {
if (this.state.loading) {
return (
<div className="loading mt-32 mx-auto"><div></div></div>
)
}
if (this.state.countries) {
return (
<>
<h1 className="text-xl font-bold dark:text-gray-100">Top countries</h1>
<div className="my-4 border-b border-gray-300 dark:border-gray-500"></div>
<main className="modal__content">
<table className="w-max overflow-x-auto md:w-full table-striped table-fixed">
<thead>
<tr>
<th
className="p-2 w-48 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="left"
>
Country
</th>
{this.showConversionRate() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">Total visitors</th>}
<th
// eslint-disable-next-line max-len
className="p-2 w-32 lg:w-1/2 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400"
align="right"
>
{this.label()}
</th>
{this.showConversionRate() && <th className="p-2 w-32 text-xs tracking-wide font-bold text-gray-500 dark:text-gray-400" align="right">CR</th>}
</tr>
</thead>
<tbody>
{ this.state.countries.map(this.renderCountry.bind(this)) }
</tbody>
</table>
</main>
</>
)
}
return null
}
render() {
return (
<Modal site={this.props.site} show={!this.state.loading}>
{ this.renderBody() }
</Modal>
)
}
}
export default withRouter(CountriesModal)

View File

@ -32,7 +32,19 @@ export default class ListReport extends React.Component {
}
label() {
return this.props.query.period === 'realtime' ? 'Current visitors' : 'Visitors'
if (this.props.query.period === 'realtime') {
return 'Current visitors'
}
if (this.showConversionRate()) {
return 'Conversions'
}
return 'Visitors'
}
showConversionRate() {
return !!this.props.query.filters.goal
}
renderListItem(listItem) {
@ -42,27 +54,34 @@ export default class ListReport extends React.Component {
query.set(key, listItem[valueKey])
}))
const maxWidthDeduction = this.showConversionRate() ? "10rem" : "5rem"
const lightBackground = this.props.color || 'bg-green-50'
return (
<div className="flex items-center justify-between my-1 text-sm" key={listItem.name}>
<Bar
count={listItem.visitors}
all={this.state.list}
bg="bg-green-50 dark:bg-gray-500 dark:bg-opacity-15"
maxWidthDeduction="6rem"
bg={`${lightBackground} 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">
<span className="flex px-2 py-1.5 dark:text-gray-300 relative z-9 break-all" tooltip={this.props.tooltipText && this.props.tooltipText(listItem)}>
<Link className="md:truncate block hover:underline" to={{search: query.toString()}}>
{this.props.renderIcon && this.props.renderIcon(listItem)}
{this.props.renderIcon && ' '}
{listItem.name}
</Link>
</span>
</Bar>
<span className="font-medium dark:text-gray-200">
<span className="font-medium dark:text-gray-200 w-20 text-right">
{numberFormatter(listItem.visitors)}
{
listItem.percentage &&
<span className="inline-block w-8 text-xs text-right">({listItem.percentage}%)</span>
listItem.percentage >= 0
? <span className="inline-block w-8 text-xs text-right">({listItem.percentage}%)</span>
: null
}
</span>
{this.showConversionRate() && <span className="font-medium dark:text-gray-200 w-20 text-right">{listItem.conversion_rate}%</span>}
</div>
)
}
@ -73,7 +92,10 @@ export default class ListReport extends React.Component {
<>
<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>{ this.props.keyLabel }</span>
<span>{ this.label() }</span>
<span className="text-right">
<span className="inline-block w-20">{this.label()}</span>
{this.showConversionRate() && <span className="inline-block w-20">CR</span>}
</span>
</div>
{ this.state.list && this.state.list.map(this.renderListItem.bind(this)) }
</>

View File

@ -487,14 +487,14 @@ defmodule PlausibleWeb.Api.StatsController do
countries =
Stats.breakdown(site, query, "visit:country", ["visitors"], pagination)
|> maybe_add_cr(site, query, {300, 1}, "country", "visit:country")
|> transform_keys(%{"country" => "name"})
|> transform_keys(%{"country" => "code"})
|> maybe_add_percentages(query)
if params["csv"] do
countries =
countries
|> Enum.map(fn country ->
iso3166 = Stats.CountryName.from_iso3166(country["name"])
iso3166 = Stats.CountryName.from_iso3166(country["code"])
Map.put(country, "name", iso3166)
end)
@ -508,8 +508,9 @@ defmodule PlausibleWeb.Api.StatsController do
else
countries =
Enum.map(countries, fn country ->
alpha3 = Stats.CountryName.to_alpha3(country["name"])
Map.put(country, "name", alpha3)
name = Stats.CountryName.from_iso3166(country["code"])
code = Stats.CountryName.to_alpha3(country["code"])
Map.merge(country, %{"name" => name, "code" => code})
end)
json(conn, countries)

View File

@ -22,12 +22,14 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
assert json_response(conn, 200) == [
%{
"name" => "EST",
"code" => "EST",
"name" => "Estonia",
"visitors" => 2,
"percentage" => 67
},
%{
"name" => "GBR",
"code" => "GBR",
"name" => "United Kingdom",
"visitors" => 1,
"percentage" => 33
}
@ -58,13 +60,15 @@ defmodule PlausibleWeb.Api.StatsController.CountriesTest do
assert json_response(conn, 200) == [
%{
"name" => "GBR",
"code" => "GBR",
"name" => "United Kingdom",
"total_visitors" => 1,
"visitors" => 1,
"conversion_rate" => 100.0
},
%{
"name" => "EST",
"code" => "EST",
"name" => "Estonia",
"total_visitors" => 2,
"visitors" => 1,
"conversion_rate" => 50.0