mirror of
https://github.com/plausible/analytics.git
synced 2024-12-23 17:44:43 +03:00
Realtime dashboard improvements (#2445)
* add a new realtime-update-timer module * hook to the new 'tick' event in ListReport for auto-updates This commit fixes the bug where all reports using the `ListReport` component did not auto-update in realtime mode. Those reports are: - Pages (Top / Entry / Exit) - Locations (Countries / Regions / Cities) - Devices (Screen Sizes / Browsers + versions / OS-s + versions) * fetch data for ListReports only when scrolled into view * refactor fetching data in ListReport * refer to one source of truth for utm tags * make the 'All' tab in Sources auto-update * make all UTM tabs in Sources auto-update * fetch UTM data only when scrolled into view * auto-update Referrers with the new timer * auto-update google search terms * auto-update Conversions * make countries map auto-update * auto-update visitor-graph and top stats with new timer * use new tick event for current visitors (in Historical) * remove the old timer class * update changelog * Visual improvements to automatic realtime updates (#2532) * minor consistency fix for text color in dark mode * use FlipMove in goal conversions report * use FlipMove in ListReports * set main graph and top stats loading state correctly * refactor isIntervalValid function * enforce intervals are valid when set and stored * remove duplicate data fetching on interval change Fetching new data is handled by the `fetchGraphData` callback in `updateInterval` * refactor updateMetric function * make it clearer why 'metric' can be a faulty value * extract 'query' and 'site' variables from 'this.props' * reset interval state only when period is changed The 'maybeRollbackInterval' function was also used to fetch data. This commit replaces all those function calls with 'fetchGraphData' which better describes the actual behavior. We should only worry about rolling back the interval if 'query.period' has changed. This commit also stops the graph from flickering when it is updated in realtime. * update names of two variables * remove unnecessary negation * make collapsed graph state more explicit * consider stored invalid intervals when graph mounts * fix not showing loading spinner regression * remove interval state from VisitorGraph (#2540) * Realtime prop breakdown (#2535) * disable load more in realtime mode * extract doFetch function * separate fetchPropBreakdown and fetchNextPage functions * subscribe for auto-updates in realtime * improve readability with function name changes
This commit is contained in:
parent
22e2ae1844
commit
47e21121db
@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Automatically update all visible dashboard reports in the realtime view
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Reject events with long URIs and data URIs plausible/analytics#2536
|
- Reject events with long URIs and data URIs plausible/analytics#2536
|
||||||
- Always show direct traffic in sources reports plausible/analytics#2531
|
- Always show direct traffic in sources reports plausible/analytics#2531
|
||||||
|
@ -32,7 +32,7 @@ function Historical(props) {
|
|||||||
<div className="items-center w-full flex">
|
<div className="items-center w-full flex">
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<SiteSwitcher site={props.site} loggedIn={props.loggedIn} currentUserRole={props.currentUserRole} />
|
<SiteSwitcher site={props.site} loggedIn={props.loggedIn} currentUserRole={props.currentUserRole} />
|
||||||
<CurrentVisitors timer={props.timer} site={props.site} query={props.query} />
|
<CurrentVisitors site={props.site} query={props.query} />
|
||||||
<Filters className="flex" site={props.site} query={props.query} history={props.history} />
|
<Filters className="flex" site={props.site} query={props.query} history={props.history} />
|
||||||
</div>
|
</div>
|
||||||
<Datepicker site={props.site} query={props.query} />
|
<Datepicker site={props.site} query={props.query} />
|
||||||
|
@ -7,31 +7,11 @@ import {parseQuery} from './query'
|
|||||||
import * as api from './api'
|
import * as api from './api'
|
||||||
import { withComparisonProvider } from './comparison-provider-hoc';
|
import { withComparisonProvider } from './comparison-provider-hoc';
|
||||||
|
|
||||||
const THIRTY_SECONDS = 30000
|
|
||||||
|
|
||||||
class Timer {
|
|
||||||
constructor() {
|
|
||||||
this.listeners = []
|
|
||||||
this.intervalId = setInterval(this.dispatchTick.bind(this), THIRTY_SECONDS)
|
|
||||||
}
|
|
||||||
|
|
||||||
onTick(listener) {
|
|
||||||
this.listeners.push(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatchTick() {
|
|
||||||
for (const listener of this.listeners) {
|
|
||||||
listener()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Dashboard extends React.Component {
|
class Dashboard extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {
|
this.state = {
|
||||||
query: parseQuery(props.location.search, this.props.site),
|
query: parseQuery(props.location.search, this.props.site),
|
||||||
timer: new Timer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,9 +24,9 @@ class Dashboard extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (this.state.query.period === 'realtime') {
|
if (this.state.query.period === 'realtime') {
|
||||||
return <Realtime timer={this.state.timer} site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
|
return <Realtime site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
|
||||||
} else {
|
} else {
|
||||||
return <Historical timer={this.state.timer} site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
|
return <Historical site={this.props.site} loggedIn={this.props.loggedIn} currentUserRole={this.props.currentUserRole} query={this.state.query} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,9 @@ import 'url-search-params-polyfill';
|
|||||||
import Router from './router'
|
import Router from './router'
|
||||||
import ErrorBoundary from './error-boundary'
|
import ErrorBoundary from './error-boundary'
|
||||||
import * as api from './api'
|
import * as api from './api'
|
||||||
|
import * as timer from './util/realtime-update-timer'
|
||||||
|
|
||||||
|
timer.start()
|
||||||
|
|
||||||
const container = document.getElementById('stats-react-container')
|
const container = document.getElementById('stats-react-container')
|
||||||
|
|
||||||
|
@ -39,14 +39,14 @@ class Realtime extends React.Component {
|
|||||||
<Datepicker site={this.props.site} query={this.props.query} />
|
<Datepicker site={this.props.site} query={this.props.query} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VisitorGraph site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
<VisitorGraph site={this.props.site} query={this.props.query} />
|
||||||
<div className="items-start justify-between block w-full md:flex">
|
<div className="items-start justify-between block w-full md:flex">
|
||||||
<Sources site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
<Sources site={this.props.site} query={this.props.query} />
|
||||||
<Pages site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
<Pages site={this.props.site} query={this.props.query} />
|
||||||
</div>
|
</div>
|
||||||
<div className="items-start justify-between block w-full md:flex">
|
<div className="items-start justify-between block w-full md:flex">
|
||||||
<Locations site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
<Locations site={this.props.site} query={this.props.query} />
|
||||||
<Devices site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
<Devices site={this.props.site} query={this.props.query} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ this.renderConversions() }
|
{ this.renderConversions() }
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import FlipMove from 'react-flip-move'
|
||||||
|
|
||||||
|
|
||||||
import Bar from '../bar'
|
import Bar from '../bar'
|
||||||
import PropBreakdown from './prop-breakdown'
|
import PropBreakdown from './prop-breakdown'
|
||||||
@ -20,7 +22,7 @@ export default class Conversions extends React.Component {
|
|||||||
viewport: DEFAULT_WIDTH,
|
viewport: DEFAULT_WIDTH,
|
||||||
}
|
}
|
||||||
this.onVisible = this.onVisible.bind(this)
|
this.onVisible = this.onVisible.bind(this)
|
||||||
|
this.fetchConversions = this.fetchConversions.bind(this)
|
||||||
this.handleResize = this.handleResize.bind(this);
|
this.handleResize = this.handleResize.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ export default class Conversions extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.handleResize, false);
|
window.removeEventListener('resize', this.handleResize, false);
|
||||||
|
document.removeEventListener('tick', this.fetchConversions)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize() {
|
handleResize() {
|
||||||
@ -39,6 +42,9 @@ export default class Conversions extends React.Component {
|
|||||||
|
|
||||||
onVisible() {
|
onVisible() {
|
||||||
this.fetchConversions()
|
this.fetchConversions()
|
||||||
|
if (this.props.query.period === 'realtime') {
|
||||||
|
document.addEventListener('tick', this.fetchConversions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -102,8 +108,9 @@ export default class Conversions extends React.Component {
|
|||||||
<span className="inline-block w-20">CR</span>
|
<span className="inline-block w-20">CR</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<FlipMove>
|
||||||
{ this.state.goals.map(this.renderGoal.bind(this)) }
|
{ this.state.goals.map(this.renderGoal.bind(this)) }
|
||||||
|
</FlipMove>
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import * as api from '../../api'
|
|||||||
|
|
||||||
const MOBILE_UPPER_WIDTH = 767
|
const MOBILE_UPPER_WIDTH = 767
|
||||||
const DEFAULT_WIDTH = 1080
|
const DEFAULT_WIDTH = 1080
|
||||||
|
const BREAKDOWN_LIMIT = 100
|
||||||
|
|
||||||
// https://stackoverflow.com/a/43467144
|
// https://stackoverflow.com/a/43467144
|
||||||
function isValidHttpUrl(string) {
|
function isValidHttpUrl(string) {
|
||||||
@ -45,17 +46,25 @@ export default class PropertyBreakdown extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.handleResize = this.handleResize.bind(this);
|
this.handleResize = this.handleResize.bind(this);
|
||||||
|
this.fetch = this.fetch.bind(this)
|
||||||
|
this.fetchAndReplace = this.fetchAndReplace.bind(this)
|
||||||
|
this.fetchAndConcat = this.fetchAndConcat.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
window.addEventListener('resize', this.handleResize, false);
|
window.addEventListener('resize', this.handleResize, false);
|
||||||
|
|
||||||
this.handleResize();
|
this.handleResize();
|
||||||
this.fetchPropBreakdown()
|
this.fetchAndReplace()
|
||||||
|
|
||||||
|
if (this.props.query.period === 'realtime') {
|
||||||
|
document.addEventListener('tick', this.fetchAndReplace)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.handleResize, false);
|
window.removeEventListener('resize', this.handleResize, false);
|
||||||
|
document.removeEventListener('tick', this.fetchAndReplace)
|
||||||
}
|
}
|
||||||
|
|
||||||
handleResize() {
|
handleResize() {
|
||||||
@ -67,19 +76,31 @@ export default class PropertyBreakdown extends React.Component {
|
|||||||
return viewport > MOBILE_UPPER_WIDTH ? "16rem" : "10rem";
|
return viewport > MOBILE_UPPER_WIDTH ? "16rem" : "10rem";
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPropBreakdown() {
|
fetch({concat}) {
|
||||||
if (this.props.query.filters['goal']) {
|
if (!this.props.query.filters['goal']) return
|
||||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/property/${encodeURIComponent(this.state.propKey)}`, this.props.query, {limit: 100, page: this.state.page})
|
|
||||||
.then((res) => this.setState((state) => ({
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/property/${encodeURIComponent(this.state.propKey)}`, this.props.query, {limit: BREAKDOWN_LIMIT, page: this.state.page})
|
||||||
loading: false,
|
.then((res) => {
|
||||||
breakdown: state.breakdown.concat(res),
|
let breakdown = concat ? this.state.breakdown.concat(res) : res
|
||||||
moreResultsAvailable: res.length === 100
|
|
||||||
})))
|
this.setState(() => ({
|
||||||
}
|
loading: false,
|
||||||
|
breakdown: breakdown,
|
||||||
|
moreResultsAvailable: res.length >= BREAKDOWN_LIMIT
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAndReplace() {
|
||||||
|
this.fetch({concat: false})
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchAndConcat() {
|
||||||
|
this.fetch({concat: true})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMore() {
|
loadMore() {
|
||||||
this.setState({loading: true, page: this.state.page + 1}, this.fetchPropBreakdown.bind(this))
|
this.setState({loading: true, page: this.state.page + 1}, this.fetchAndConcat.bind(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
renderUrl(value) {
|
renderUrl(value) {
|
||||||
@ -143,13 +164,13 @@ export default class PropertyBreakdown extends React.Component {
|
|||||||
|
|
||||||
changePropKey(newKey) {
|
changePropKey(newKey) {
|
||||||
storage.setItem(this.storageKey, newKey)
|
storage.setItem(this.storageKey, newKey)
|
||||||
this.setState({propKey: newKey, loading: true, breakdown: [], page: 1, moreResultsAvailable: false}, this.fetchPropBreakdown)
|
this.setState({propKey: newKey, loading: true, breakdown: [], page: 1, moreResultsAvailable: false}, this.fetchAndReplace)
|
||||||
}
|
}
|
||||||
|
|
||||||
renderLoading() {
|
renderLoading() {
|
||||||
if (this.state.loading) {
|
if (this.state.loading) {
|
||||||
return <div className="px-4 py-2"><div className="loading sm mx-auto"><div></div></div></div>
|
return <div className="px-4 py-2"><div className="loading sm mx-auto"><div></div></div></div>
|
||||||
} else if (this.state.moreResultsAvailable) {
|
} else if (this.state.moreResultsAvailable && this.props.query.period !== 'realtime') {
|
||||||
return (
|
return (
|
||||||
<div className="w-full text-center my-4">
|
<div className="w-full text-center my-4">
|
||||||
<button onClick={this.loadMore.bind(this)} type="button" className="button">
|
<button onClick={this.loadMore.bind(this)} type="button" className="button">
|
||||||
|
@ -8,11 +8,16 @@ export default class CurrentVisitors extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {currentVisitors: null}
|
this.state = {currentVisitors: null}
|
||||||
|
this.updateCount = this.updateCount.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.updateCount()
|
this.updateCount()
|
||||||
this.props.timer.onTick(this.updateCount.bind(this))
|
document.addEventListener('tick', this.updateCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('tick', this.updateCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCount() {
|
updateCount() {
|
||||||
|
@ -139,18 +139,18 @@ export default class Devices extends React.Component {
|
|||||||
switch (this.state.mode) {
|
switch (this.state.mode) {
|
||||||
case 'browser':
|
case 'browser':
|
||||||
if (this.props.query.filters.browser) {
|
if (this.props.query.filters.browser) {
|
||||||
return <BrowserVersions site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
return <BrowserVersions site={this.props.site} query={this.props.query} />
|
||||||
}
|
}
|
||||||
return <Browsers site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
return <Browsers site={this.props.site} query={this.props.query} />
|
||||||
case 'os':
|
case 'os':
|
||||||
if (this.props.query.filters.os) {
|
if (this.props.query.filters.os) {
|
||||||
return <OperatingSystemVersions site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
return <OperatingSystemVersions site={this.props.site} query={this.props.query} />
|
||||||
}
|
}
|
||||||
return <OperatingSystems site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
return <OperatingSystems site={this.props.site} query={this.props.query} />
|
||||||
case 'size':
|
case 'size':
|
||||||
default:
|
default:
|
||||||
return (
|
return (
|
||||||
<ScreenSizes site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
<ScreenSizes site={this.props.site} query={this.props.query} />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import numberFormatter, {durationFormatter} from '../../util/number-formatter'
|
import numberFormatter, {durationFormatter} from '../../util/number-formatter'
|
||||||
import dateFormatter from './date-formatter.js'
|
import dateFormatter from './date-formatter.js'
|
||||||
|
|
||||||
export const INTERVALS = ["month", "week", "date", "hour", "minute"]
|
|
||||||
|
|
||||||
export const METRIC_MAPPING = {
|
export const METRIC_MAPPING = {
|
||||||
'Unique visitors (last 30 min)': 'visitors',
|
'Unique visitors (last 30 min)': 'visitors',
|
||||||
'Pageviews (last 30 min)': 'pageviews',
|
'Pageviews (last 30 min)': 'pageviews',
|
||||||
|
@ -5,7 +5,7 @@ import { navigateToQuery } from '../../query'
|
|||||||
import * as api from '../../api'
|
import * as api from '../../api'
|
||||||
import * as storage from '../../util/storage'
|
import * as storage from '../../util/storage'
|
||||||
import LazyLoader from '../../components/lazy-loader'
|
import LazyLoader from '../../components/lazy-loader'
|
||||||
import {GraphTooltip, buildDataSet, INTERVALS, METRIC_MAPPING, METRIC_LABELS, METRIC_FORMATTER} from './graph-util';
|
import {GraphTooltip, buildDataSet, METRIC_MAPPING, METRIC_LABELS, METRIC_FORMATTER} from './graph-util';
|
||||||
import dateFormatter from './date-formatter';
|
import dateFormatter from './date-formatter';
|
||||||
import TopStats from './top-stats';
|
import TopStats from './top-stats';
|
||||||
import { IntervalPicker, getStoredInterval, storeInterval } from './interval-picker';
|
import { IntervalPicker, getStoredInterval, storeInterval } from './interval-picker';
|
||||||
@ -233,7 +233,7 @@ class LineGraph extends React.Component {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
const interval = this.props.graphData?.interval || this.state.interval
|
const interval = this.props.graphData?.interval
|
||||||
const queryParams = api.serializeQuery(this.props.query, [{ interval }])
|
const queryParams = api.serializeQuery(this.props.query, [{ interval }])
|
||||||
const endpoint = `/${encodeURIComponent(this.props.site.domain)}/export${queryParams}`
|
const endpoint = `/${encodeURIComponent(this.props.site.domain)}/export${queryParams}`
|
||||||
|
|
||||||
@ -314,81 +314,71 @@ export default class VisitorGraph extends React.Component {
|
|||||||
this.state = {
|
this.state = {
|
||||||
topStatsLoadingState: LOADING_STATE.loading,
|
topStatsLoadingState: LOADING_STATE.loading,
|
||||||
mainGraphLoadingState: LOADING_STATE.loading,
|
mainGraphLoadingState: LOADING_STATE.loading,
|
||||||
metric: storage.getItem(`metric__${this.props.site.domain}`) || 'visitors',
|
metric: storage.getItem(`metric__${this.props.site.domain}`) || 'visitors'
|
||||||
interval: getStoredInterval(this.props.query.period, this.props.site.domain)
|
|
||||||
}
|
}
|
||||||
this.onVisible = this.onVisible.bind(this)
|
this.onVisible = this.onVisible.bind(this)
|
||||||
this.updateMetric = this.updateMetric.bind(this)
|
this.updateMetric = this.updateMetric.bind(this)
|
||||||
this.fetchTopStatData = this.fetchTopStatData.bind(this)
|
this.fetchTopStatData = this.fetchTopStatData.bind(this)
|
||||||
this.fetchGraphData = this.fetchGraphData.bind(this)
|
this.fetchGraphData = this.fetchGraphData.bind(this)
|
||||||
this.maybeRollbackInterval = this.maybeRollbackInterval.bind(this)
|
|
||||||
this.updateInterval = this.updateInterval.bind(this)
|
this.updateInterval = this.updateInterval.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
isIntervalValid({ query, site }) {
|
isIntervalValid(interval) {
|
||||||
const period = query?.period
|
const { query, site } = this.props
|
||||||
const validIntervalsForPeriod = site.validIntervalsByPeriod[period] || []
|
const validIntervals = site.validIntervalsByPeriod[query.period] || []
|
||||||
const storedInterval = getStoredInterval(period, site.domain)
|
|
||||||
|
|
||||||
return validIntervalsForPeriod.includes(storedInterval)
|
return validIntervals.includes(interval)
|
||||||
}
|
}
|
||||||
|
|
||||||
maybeRollbackInterval() {
|
getIntervalFromStorage() {
|
||||||
if (this.isIntervalValid(this.props)) {
|
const { query, site } = this.props
|
||||||
const interval = getStoredInterval(this.props.query.period, this.props.site.domain)
|
const storedInterval = getStoredInterval(query.period, site.domain)
|
||||||
|
|
||||||
this.setState({interval}, () => {
|
if (this.isIntervalValid(storedInterval)) {
|
||||||
this.fetchGraphData()
|
return storedInterval
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({interval: undefined}, () => {
|
return null
|
||||||
this.setState({graphData: null})
|
|
||||||
this.fetchGraphData()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateInterval(interval) {
|
updateInterval(interval) {
|
||||||
if (INTERVALS.includes(interval)) {
|
if (this.isIntervalValid(interval)) {
|
||||||
this.setState({interval, mainGraphLoadingState: LOADING_STATE.refreshing}, this.maybeRollbackInterval)
|
|
||||||
storeInterval(this.props.query.period, this.props.site.domain, interval)
|
storeInterval(this.props.query.period, this.props.site.domain, interval)
|
||||||
|
this.setState({ mainGraphLoadingState: LOADING_STATE.refreshing }, this.fetchGraphData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisible() {
|
onVisible() {
|
||||||
this.setState({mainGraphLoadingState: LOADING_STATE.loading}, this.maybeRollbackInterval)
|
this.setState({mainGraphLoadingState: LOADING_STATE.loading}, this.fetchGraphData)
|
||||||
this.fetchTopStatData()
|
this.fetchTopStatData()
|
||||||
if (this.props.timer) {
|
if (this.props.query.period === 'realtime') {
|
||||||
this.props.timer.onTick(this.maybeRollbackInterval)
|
document.addEventListener('tick', this.fetchGraphData)
|
||||||
this.props.timer.onTick(this.fetchTopStatData)
|
document.addEventListener('tick', this.fetchTopStatData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps, prevState) {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
const { metric, topStatData, interval } = this.state;
|
const { metric, topStatData } = this.state;
|
||||||
|
const { query, site } = this.props
|
||||||
|
|
||||||
if (this.props.query !== prevProps.query) {
|
if (query !== prevProps.query) {
|
||||||
if (metric) {
|
if (this.isGraphCollapsed()) {
|
||||||
this.setState({ mainGraphLoadingState: LOADING_STATE.loading, topStatsLoadingState: LOADING_STATE.loading, graphData: null, topStatData: null }, this.maybeRollbackInterval)
|
|
||||||
} else {
|
|
||||||
this.setState({ topStatsLoadingState: LOADING_STATE.loading, topStatData: null })
|
this.setState({ topStatsLoadingState: LOADING_STATE.loading, topStatData: null })
|
||||||
|
} else {
|
||||||
|
this.setState({ mainGraphLoadingState: LOADING_STATE.loading, topStatsLoadingState: LOADING_STATE.loading, graphData: null, topStatData: null }, this.fetchGraphData)
|
||||||
}
|
}
|
||||||
this.fetchTopStatData()
|
this.fetchTopStatData()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (metric !== prevState.metric) {
|
if (metric !== prevState.metric) {
|
||||||
this.setState({mainGraphLoadingState: LOADING_STATE.refreshing}, this.maybeRollbackInterval)
|
this.setState({mainGraphLoadingState: LOADING_STATE.refreshing}, this.fetchGraphData)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interval !== prevState.interval && interval) {
|
const savedMetric = storage.getItem(`metric__${site.domain}`)
|
||||||
this.setState({mainGraphLoadingState: LOADING_STATE.refreshing}, this.maybeRollbackInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
const savedMetric = storage.getItem(`metric__${this.props.site.domain}`)
|
|
||||||
const topStatLabels = topStatData && topStatData.top_stats.map(({ name }) => METRIC_MAPPING[name]).filter(name => name)
|
const topStatLabels = topStatData && topStatData.top_stats.map(({ name }) => METRIC_MAPPING[name]).filter(name => name)
|
||||||
const prevTopStatLabels = prevState.topStatData && prevState.topStatData.top_stats.map(({ name }) => METRIC_MAPPING[name]).filter(name => name)
|
const prevTopStatLabels = prevState.topStatData && prevState.topStatData.top_stats.map(({ name }) => METRIC_MAPPING[name]).filter(name => name)
|
||||||
if (topStatLabels && `${topStatLabels}` !== `${prevTopStatLabels}`) {
|
if (topStatLabels && `${topStatLabels}` !== `${prevTopStatLabels}`) {
|
||||||
if (this.props.query.filters.goal && metric !== 'conversions') {
|
if (query.filters.goal && metric !== 'conversions') {
|
||||||
this.setState({ metric: 'conversions' })
|
this.setState({ metric: 'conversions' })
|
||||||
} else if (topStatLabels.includes(savedMetric) && savedMetric !== "") {
|
} else if (topStatLabels.includes(savedMetric) && savedMetric !== "") {
|
||||||
this.setState({ metric: savedMetric })
|
this.setState({ metric: savedMetric })
|
||||||
@ -398,41 +388,48 @@ export default class VisitorGraph extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMetric(newMetric) {
|
isGraphCollapsed() {
|
||||||
if (newMetric === this.state.metric) {
|
return this.state.metric === ""
|
||||||
storage.setItem(`metric__${this.props.site.domain}`, "")
|
}
|
||||||
this.setState({ metric: "" })
|
|
||||||
} else {
|
componentWillUnmount() {
|
||||||
storage.setItem(`metric__${this.props.site.domain}`, newMetric)
|
document.removeEventListener('tick', this.fetchGraphData)
|
||||||
this.setState({ metric: newMetric })
|
document.removeEventListener('tick', this.fetchTopStatData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateMetric(clickedMetric) {
|
||||||
|
const newMetric = clickedMetric === this.state.metric ? "" : clickedMetric
|
||||||
|
|
||||||
|
storage.setItem(`metric__${this.props.site.domain}`, newMetric)
|
||||||
|
this.setState({ metric: newMetric })
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchGraphData() {
|
fetchGraphData() {
|
||||||
if (!this.state.metric) {
|
if (this.isGraphCollapsed()) {
|
||||||
this.setState({ mainGraphLoadingState: LOADING_STATE.ready, graphData: null })
|
this.setState({ mainGraphLoadingState: LOADING_STATE.loaded, graphData: null })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = `/api/stats/${encodeURIComponent(this.props.site.domain)}/main-graph`
|
const url = `/api/stats/${encodeURIComponent(this.props.site.domain)}/main-graph`
|
||||||
let params = {metric: this.state.metric || 'none'}
|
let params = { metric: this.state.metric }
|
||||||
if (this.state.interval) { params.interval = this.state.interval }
|
const interval = this.getIntervalFromStorage()
|
||||||
|
if (interval) { params.interval = interval }
|
||||||
|
|
||||||
api.get(url, this.props.query, params)
|
api.get(url, this.props.query, params)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.setState({ mainGraphLoadingState: LOADING_STATE.ready, graphData: res })
|
this.setState({ mainGraphLoadingState: LOADING_STATE.loaded, graphData: res })
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.log(err)
|
console.log(err)
|
||||||
this.setState({ mainGraphLoadingState: LOADING_STATE.ready, graphData: false })
|
this.setState({ mainGraphLoadingState: LOADING_STATE.loaded, graphData: false })
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchTopStatData() {
|
fetchTopStatData() {
|
||||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/top-stats`, this.props.query)
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/top-stats`, this.props.query)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
this.setState({ topStatsLoadingState: LOADING_STATE.ready, topStatData: res })
|
this.setState({ topStatsLoadingState: LOADING_STATE.loaded, topStatData: res })
|
||||||
return res
|
return res
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -443,14 +440,14 @@ export default class VisitorGraph extends React.Component {
|
|||||||
|
|
||||||
const theme = document.querySelector('html').classList.contains('dark') || false
|
const theme = document.querySelector('html').classList.contains('dark') || false
|
||||||
|
|
||||||
const topStatsReadyOrRefreshing = (topStatsLoadingState === LOADING_STATE.ready || topStatsLoadingState === LOADING_STATE.refreshing)
|
const topStatsLoadedOrRefreshing = (topStatsLoadingState === LOADING_STATE.loaded || topStatsLoadingState === LOADING_STATE.refreshing)
|
||||||
const mainGraphReadyOrRefreshing = (mainGraphLoadingState === LOADING_STATE.ready || mainGraphLoadingState === LOADING_STATE.refreshing)
|
const mainGraphLoadedOrRefreshing = (mainGraphLoadingState === LOADING_STATE.loaded || mainGraphLoadingState === LOADING_STATE.refreshing)
|
||||||
const noMetricOrRefreshing = (!metric || mainGraphLoadingState === LOADING_STATE.refreshing)
|
const noMetricOrRefreshing = (!metric || mainGraphLoadingState === LOADING_STATE.refreshing)
|
||||||
const topStatAndGraphLoaded = !!(topStatData && graphData)
|
const topStatAndGraphLoaded = !!(topStatData && graphData)
|
||||||
|
|
||||||
const showGraph =
|
const showGraph =
|
||||||
topStatsReadyOrRefreshing &&
|
topStatsLoadedOrRefreshing &&
|
||||||
mainGraphReadyOrRefreshing &&
|
mainGraphLoadedOrRefreshing &&
|
||||||
(topStatData && noMetricOrRefreshing || topStatAndGraphLoaded)
|
(topStatData && noMetricOrRefreshing || topStatAndGraphLoaded)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -131,14 +131,14 @@ export default class Locations extends React.Component {
|
|||||||
renderContent() {
|
renderContent() {
|
||||||
switch(this.state.mode) {
|
switch(this.state.mode) {
|
||||||
case "cities":
|
case "cities":
|
||||||
return <Cities site={this.props.site} query={this.props.query} timer={this.props.timer}/>
|
return <Cities site={this.props.site} query={this.props.query} />
|
||||||
case "regions":
|
case "regions":
|
||||||
return <Regions onClick={this.onRegionFilter} site={this.props.site} query={this.props.query} timer={this.props.timer}/>
|
return <Regions onClick={this.onRegionFilter} site={this.props.site} query={this.props.query} />
|
||||||
case "countries":
|
case "countries":
|
||||||
return <Countries onClick={this.onCountryFilter('countries')} site={this.props.site} query={this.props.query} timer={this.props.timer}/>
|
return <Countries onClick={this.onCountryFilter('countries')} site={this.props.site} query={this.props.query} />
|
||||||
case "map":
|
case "map":
|
||||||
default:
|
default:
|
||||||
return <CountriesMap onClick={this.onCountryFilter('map')} site={this.props.site} query={this.props.query} timer={this.props.timer}/>
|
return <CountriesMap onClick={this.onCountryFilter('map')} site={this.props.site} query={this.props.query}/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ class Countries extends React.Component {
|
|||||||
darkTheme: document.querySelector('html').classList.contains('dark') || false
|
darkTheme: document.querySelector('html').classList.contains('dark') || false
|
||||||
}
|
}
|
||||||
this.onVisible = this.onVisible.bind(this)
|
this.onVisible = this.onVisible.bind(this)
|
||||||
|
this.updateCountries = this.updateCountries.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -33,12 +34,15 @@ class Countries extends React.Component {
|
|||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
window.removeEventListener('resize', this.resizeMap);
|
window.removeEventListener('resize', this.resizeMap);
|
||||||
|
document.removeEventListener('tick', this.updateCountries);
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisible() {
|
onVisible() {
|
||||||
this.fetchCountries().then(this.drawMap.bind(this))
|
this.fetchCountries().then(this.drawMap.bind(this))
|
||||||
window.addEventListener('resize', this.resizeMap);
|
window.addEventListener('resize', this.resizeMap);
|
||||||
if (this.props.timer) this.props.timer.onTick(this.updateCountries.bind(this))
|
if (this.props.query.period === 'realtime') {
|
||||||
|
document.addEventListener('tick', this.updateCountries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getDataset() {
|
getDataset() {
|
||||||
|
@ -101,12 +101,12 @@ export default class Pages extends React.Component {
|
|||||||
renderContent() {
|
renderContent() {
|
||||||
switch(this.state.mode) {
|
switch(this.state.mode) {
|
||||||
case "entry-pages":
|
case "entry-pages":
|
||||||
return <EntryPages site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
return <EntryPages site={this.props.site} query={this.props.query} />
|
||||||
case "exit-pages":
|
case "exit-pages":
|
||||||
return <ExitPages site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
return <ExitPages site={this.props.site} query={this.props.query} />
|
||||||
case "pages":
|
case "pages":
|
||||||
default:
|
default:
|
||||||
return <TopPages site={this.props.site} query={this.props.query} timer={this.props.timer} />
|
return <TopPages site={this.props.site} query={this.props.query} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
|
import FlipMove from 'react-flip-move';
|
||||||
|
|
||||||
|
|
||||||
import FadeIn from '../../fade-in'
|
import FadeIn from '../../fade-in'
|
||||||
import MoreLink from '../more-link'
|
import MoreLink from '../more-link'
|
||||||
@ -28,25 +30,32 @@ function ExternalLink({item, externalLinkDest}) {
|
|||||||
|
|
||||||
export default function ListReport(props) {
|
export default function ListReport(props) {
|
||||||
const [state, setState] = useState({loading: true, list: null})
|
const [state, setState] = useState({loading: true, list: null})
|
||||||
|
const [visible, setVisible] = useState(false)
|
||||||
const valueKey = props.valueKey || 'visitors'
|
const valueKey = props.valueKey || 'visitors'
|
||||||
const showConversionRate = !!props.query.filters.goal
|
const showConversionRate = !!props.query.filters.goal
|
||||||
const prevQuery = useRef();
|
|
||||||
|
|
||||||
function fetchData() {
|
const fetchData = useCallback(() => {
|
||||||
if (typeof(prevQuery.current) === 'undefined' || prevQuery.current !== props.query) {
|
if (props.query.period !== 'realtime') {
|
||||||
prevQuery.current = props.query;
|
setState({loading: true, list: null})
|
||||||
setState({loading: true, list: null})
|
}
|
||||||
props.fetchData()
|
props.fetchData()
|
||||||
.then((res) => setState({loading: false, list: res}))
|
.then((res) => setState({loading: false, list: res}))
|
||||||
|
}, [props.query])
|
||||||
|
|
||||||
|
function onVisible() {
|
||||||
|
setVisible(true)
|
||||||
|
if (props.query.period == 'realtime') {
|
||||||
|
document.addEventListener('tick', fetchData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (visible) { fetchData() }
|
||||||
|
}, [props.query, visible]);
|
||||||
|
|
||||||
function onVisible() {
|
useEffect(() => {
|
||||||
fetchData()
|
return () => { document.removeEventListener('tick', fetchData) }
|
||||||
if (props.timer) props.timer.onTick(fetchData)
|
}, []);
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function label() {
|
function label() {
|
||||||
if (props.query.period === 'realtime') {
|
if (props.query.period === 'realtime') {
|
||||||
@ -60,8 +69,6 @@ export default function ListReport(props) {
|
|||||||
return props.valueLabel || 'Visitors'
|
return props.valueLabel || 'Visitors'
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(fetchData, [props.query]);
|
|
||||||
|
|
||||||
function renderListItem(listItem) {
|
function renderListItem(listItem) {
|
||||||
const query = new URLSearchParams(window.location.search)
|
const query = new URLSearchParams(window.location.search)
|
||||||
|
|
||||||
@ -115,7 +122,9 @@ export default function ListReport(props) {
|
|||||||
{showConversionRate && <span className="inline-block w-20">CR</span>}
|
{showConversionRate && <span className="inline-block w-20">CR</span>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{ state.list && state.list.map(renderListItem) }
|
<FlipMove>
|
||||||
|
{ state.list.map(renderListItem) }
|
||||||
|
</FlipMove>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -23,11 +23,14 @@ export default class Referrers extends React.Component {
|
|||||||
super(props)
|
super(props)
|
||||||
this.state = { loading: true }
|
this.state = { loading: true }
|
||||||
this.onVisible = this.onVisible.bind(this)
|
this.onVisible = this.onVisible.bind(this)
|
||||||
|
this.fetchReferrers = this.fetchReferrers.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisible() {
|
onVisible() {
|
||||||
this.fetchReferrers()
|
this.fetchReferrers()
|
||||||
if (this.props.timer) this.props.timer.onTick(this.fetchReferrers.bind(this))
|
if (this.props.query.period === 'realtime') {
|
||||||
|
document.addEventListener('tick', this.fetchReferrers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -37,6 +40,10 @@ export default class Referrers extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('tick', this.fetchReferrers)
|
||||||
|
}
|
||||||
|
|
||||||
showConversionRate() {
|
showConversionRate() {
|
||||||
return !!this.props.query.filters.goal
|
return !!this.props.query.filters.goal
|
||||||
}
|
}
|
||||||
|
@ -5,15 +5,21 @@ import MoreLink from '../more-link'
|
|||||||
import numberFormatter from '../../util/number-formatter'
|
import numberFormatter from '../../util/number-formatter'
|
||||||
import RocketIcon from '../modals/rocket-icon'
|
import RocketIcon from '../modals/rocket-icon'
|
||||||
import * as api from '../../api'
|
import * as api from '../../api'
|
||||||
|
import LazyLoader from '../../components/lazy-loader'
|
||||||
|
|
||||||
export default class SearchTerms extends React.Component {
|
export default class SearchTerms extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.state = {loading: true}
|
this.state = {loading: true}
|
||||||
|
this.onVisible = this.onVisible.bind(this)
|
||||||
|
this.fetchSearchTerms = this.fetchSearchTerms.bind(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
onVisible() {
|
||||||
this.fetchSearchTerms()
|
this.fetchSearchTerms()
|
||||||
|
if (this.props.query.period === 'realtime') {
|
||||||
|
document.addEventListener('tick', this.fetchSearchTerms)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -23,6 +29,10 @@ export default class SearchTerms extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('tick', this.fetchSearchTerms)
|
||||||
|
}
|
||||||
|
|
||||||
fetchSearchTerms() {
|
fetchSearchTerms() {
|
||||||
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/Google`, this.props.query)
|
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/Google`, this.props.query)
|
||||||
.then((res) => this.setState({
|
.then((res) => this.setState({
|
||||||
@ -30,7 +40,7 @@ export default class SearchTerms extends React.Component {
|
|||||||
searchTerms: res.search_terms || [],
|
searchTerms: res.search_terms || [],
|
||||||
notConfigured: res.not_configured,
|
notConfigured: res.not_configured,
|
||||||
isAdmin: res.is_admin
|
isAdmin: res.is_admin
|
||||||
})).catch((error) =>
|
})).catch((error) =>
|
||||||
{
|
{
|
||||||
this.setState({ loading: false, searchTerms: [], notConfigured: true, error: true, isAdmin: error.payload.is_admin })
|
this.setState({ loading: false, searchTerms: [], notConfigured: true, error: true, isAdmin: error.payload.is_admin })
|
||||||
}
|
}
|
||||||
@ -122,7 +132,9 @@ export default class SearchTerms extends React.Component {
|
|||||||
>
|
>
|
||||||
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
|
{ this.state.loading && <div className="loading mt-44 mx-auto"><div></div></div> }
|
||||||
<FadeIn show={!this.state.loading} className="flex-grow">
|
<FadeIn show={!this.state.loading} className="flex-grow">
|
||||||
{ this.renderContent() }
|
<LazyLoader onVisible={this.onVisible}>
|
||||||
|
{ this.renderContent() }
|
||||||
|
</LazyLoader>
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -15,12 +15,15 @@ class AllSources extends React.Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
this.onVisible = this.onVisible.bind(this)
|
this.onVisible = this.onVisible.bind(this)
|
||||||
|
this.fetchReferrers = this.fetchReferrers.bind(this)
|
||||||
this.state = { loading: true }
|
this.state = { loading: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
onVisible() {
|
onVisible() {
|
||||||
this.fetchReferrers()
|
this.fetchReferrers()
|
||||||
if (this.props.timer) this.props.timer.onTick(this.fetchReferrers.bind(this))
|
if (this.props.query.period === 'realtime') {
|
||||||
|
document.addEventListener('tick', this.fetchReferrers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -30,6 +33,10 @@ class AllSources extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('tick', this.fetchReferrers)
|
||||||
|
}
|
||||||
|
|
||||||
showConversionRate() {
|
showConversionRate() {
|
||||||
return !!this.props.query.filters.goal
|
return !!this.props.query.filters.goal
|
||||||
}
|
}
|
||||||
@ -103,7 +110,7 @@ class AllSources extends React.Component {
|
|||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return <div className="font-medium text-center text-gray-500 mt-44">No data yet</div>
|
return <div className="font-medium text-center text-gray-500 mt-44 dark:text-gray-400">No data yet</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -144,12 +151,16 @@ const UTM_TAGS = {
|
|||||||
class UTMSources extends React.Component {
|
class UTMSources extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
this.onVisible = this.onVisible.bind(this)
|
||||||
|
this.fetchReferrers = this.fetchReferrers.bind(this)
|
||||||
this.state = { loading: true }
|
this.state = { loading: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
onVisible() {
|
||||||
this.fetchReferrers()
|
this.fetchReferrers()
|
||||||
if (this.props.timer) this.props.timer.onTick(this.fetchReferrers.bind(this))
|
if (this.props.query.period === 'realtime') {
|
||||||
|
document.addEventListener('tick', this.fetchReferrers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
@ -159,6 +170,10 @@ class UTMSources extends React.Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener('tick', this.fetchReferrers)
|
||||||
|
}
|
||||||
|
|
||||||
showNoRef() {
|
showNoRef() {
|
||||||
return this.props.query.period === 'realtime'
|
return this.props.query.period === 'realtime'
|
||||||
}
|
}
|
||||||
@ -240,7 +255,7 @@ class UTMSources extends React.Component {
|
|||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<LazyLoader onVisible={this.onVisible}>
|
||||||
<div className="flex justify-between w-full">
|
<div className="flex justify-between w-full">
|
||||||
<h3 className="font-bold dark:text-gray-100">Top Sources</h3>
|
<h3 className="font-bold dark:text-gray-100">Top Sources</h3>
|
||||||
{this.props.renderTabs()}
|
{this.props.renderTabs()}
|
||||||
@ -249,7 +264,7 @@ class UTMSources extends React.Component {
|
|||||||
<FadeIn show={!this.state.loading} className="flex flex-col flex-grow">
|
<FadeIn show={!this.state.loading} className="flex flex-col flex-grow">
|
||||||
{this.renderList()}
|
{this.renderList()}
|
||||||
</FadeIn>
|
</FadeIn>
|
||||||
</React.Fragment>
|
</LazyLoader>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,7 +304,7 @@ export default class SourceList extends React.Component {
|
|||||||
renderTabs() {
|
renderTabs() {
|
||||||
const activeClass = 'inline-block h-5 text-indigo-700 dark:text-indigo-500 font-bold active-prop-heading truncate text-left'
|
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 defaultClass = 'hover:text-indigo-600 cursor-pointer truncate text-left'
|
||||||
const dropdownOptions = ['utm_medium', 'utm_source', 'utm_campaign', 'utm_term', 'utm_content']
|
const dropdownOptions = Object.keys(UTM_TAGS)
|
||||||
let buttonText = UTM_TAGS[this.state.tab] ? UTM_TAGS[this.state.tab].label : 'Campaigns'
|
let buttonText = UTM_TAGS[this.state.tab] ? UTM_TAGS[this.state.tab].label : 'Campaigns'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -344,15 +359,7 @@ export default class SourceList extends React.Component {
|
|||||||
render() {
|
render() {
|
||||||
if (this.state.tab === 'all') {
|
if (this.state.tab === 'all') {
|
||||||
return <AllSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
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') {
|
} 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} />
|
|
||||||
} 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} />
|
|
||||||
} else if (this.state.tab === 'utm_content') {
|
|
||||||
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
|
||||||
} else if (this.state.tab === 'utm_term') {
|
|
||||||
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
return <UTMSources tab={this.state.tab} setTab={this.setTab.bind(this)} renderTabs={this.renderTabs.bind(this)} {...this.props} />
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
assets/js/dashboard/util/realtime-update-timer.js
Normal file
8
assets/js/dashboard/util/realtime-update-timer.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
const THIRTY_SECONDS = 30000
|
||||||
|
const tickEvent = new Event('tick')
|
||||||
|
|
||||||
|
export function start() {
|
||||||
|
setInterval(() => {
|
||||||
|
document.dispatchEvent(tickEvent)
|
||||||
|
}, THIRTY_SECONDS)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user