diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6f1680669..f6d7fdd3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@ All notable changes to this project will be documented in this file.
## Unreleased
+### Fixed
+- Automatically update all visible dashboard reports in the realtime view
+
### Changed
- Reject events with long URIs and data URIs plausible/analytics#2536
- Always show direct traffic in sources reports plausible/analytics#2531
diff --git a/assets/js/dashboard/historical.js b/assets/js/dashboard/historical.js
index 0174bf229..8c97ca0d0 100644
--- a/assets/js/dashboard/historical.js
+++ b/assets/js/dashboard/historical.js
@@ -32,7 +32,7 @@ function Historical(props) {
-
+
diff --git a/assets/js/dashboard/index.js b/assets/js/dashboard/index.js
index 0fe9b7964..3e539df8c 100644
--- a/assets/js/dashboard/index.js
+++ b/assets/js/dashboard/index.js
@@ -7,31 +7,11 @@ import {parseQuery} from './query'
import * as api from './api'
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 {
constructor(props) {
super(props)
this.state = {
query: parseQuery(props.location.search, this.props.site),
- timer: new Timer()
}
}
@@ -44,9 +24,9 @@ class Dashboard extends React.Component {
render() {
if (this.state.query.period === 'realtime') {
- return
+ return
} else {
- return
+ return
}
}
}
diff --git a/assets/js/dashboard/mount.js b/assets/js/dashboard/mount.js
index a94848a27..c1cac71d9 100644
--- a/assets/js/dashboard/mount.js
+++ b/assets/js/dashboard/mount.js
@@ -5,6 +5,9 @@ import 'url-search-params-polyfill';
import Router from './router'
import ErrorBoundary from './error-boundary'
import * as api from './api'
+import * as timer from './util/realtime-update-timer'
+
+timer.start()
const container = document.getElementById('stats-react-container')
diff --git a/assets/js/dashboard/realtime.js b/assets/js/dashboard/realtime.js
index 7293e7d1d..cf28c79b6 100644
--- a/assets/js/dashboard/realtime.js
+++ b/assets/js/dashboard/realtime.js
@@ -39,14 +39,14 @@ class Realtime extends React.Component {
-
+
-
-
+
+
{ this.renderConversions() }
diff --git a/assets/js/dashboard/stats/conversions/index.js b/assets/js/dashboard/stats/conversions/index.js
index caf65876f..3a18fcd1d 100644
--- a/assets/js/dashboard/stats/conversions/index.js
+++ b/assets/js/dashboard/stats/conversions/index.js
@@ -1,5 +1,7 @@
import React from 'react';
import { Link } from 'react-router-dom'
+import FlipMove from 'react-flip-move'
+
import Bar from '../bar'
import PropBreakdown from './prop-breakdown'
@@ -20,7 +22,7 @@ export default class Conversions extends React.Component {
viewport: DEFAULT_WIDTH,
}
this.onVisible = this.onVisible.bind(this)
-
+ this.fetchConversions = this.fetchConversions.bind(this)
this.handleResize = this.handleResize.bind(this);
}
@@ -31,6 +33,7 @@ export default class Conversions extends React.Component {
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false);
+ document.removeEventListener('tick', this.fetchConversions)
}
handleResize() {
@@ -39,6 +42,9 @@ export default class Conversions extends React.Component {
onVisible() {
this.fetchConversions()
+ if (this.props.query.period === 'realtime') {
+ document.addEventListener('tick', this.fetchConversions)
+ }
}
componentDidUpdate(prevProps) {
@@ -102,8 +108,9 @@ export default class Conversions extends React.Component {
CR
-
- { this.state.goals.map(this.renderGoal.bind(this)) }
+
+ { this.state.goals.map(this.renderGoal.bind(this)) }
+
)
}
diff --git a/assets/js/dashboard/stats/conversions/prop-breakdown.js b/assets/js/dashboard/stats/conversions/prop-breakdown.js
index e142c0f33..d68f8c477 100644
--- a/assets/js/dashboard/stats/conversions/prop-breakdown.js
+++ b/assets/js/dashboard/stats/conversions/prop-breakdown.js
@@ -8,6 +8,7 @@ import * as api from '../../api'
const MOBILE_UPPER_WIDTH = 767
const DEFAULT_WIDTH = 1080
+const BREAKDOWN_LIMIT = 100
// https://stackoverflow.com/a/43467144
function isValidHttpUrl(string) {
@@ -45,17 +46,25 @@ export default class PropertyBreakdown extends React.Component {
}
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() {
window.addEventListener('resize', this.handleResize, false);
this.handleResize();
- this.fetchPropBreakdown()
+ this.fetchAndReplace()
+
+ if (this.props.query.period === 'realtime') {
+ document.addEventListener('tick', this.fetchAndReplace)
+ }
}
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize, false);
+ document.removeEventListener('tick', this.fetchAndReplace)
}
handleResize() {
@@ -67,19 +76,31 @@ export default class PropertyBreakdown extends React.Component {
return viewport > MOBILE_UPPER_WIDTH ? "16rem" : "10rem";
}
- fetchPropBreakdown() {
- if (this.props.query.filters['goal']) {
- api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/property/${encodeURIComponent(this.state.propKey)}`, this.props.query, {limit: 100, page: this.state.page})
- .then((res) => this.setState((state) => ({
- loading: false,
- breakdown: state.breakdown.concat(res),
- moreResultsAvailable: res.length === 100
- })))
- }
+ fetch({concat}) {
+ 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: BREAKDOWN_LIMIT, page: this.state.page})
+ .then((res) => {
+ let breakdown = concat ? this.state.breakdown.concat(res) : res
+
+ this.setState(() => ({
+ loading: false,
+ breakdown: breakdown,
+ moreResultsAvailable: res.length >= BREAKDOWN_LIMIT
+ }))
+ })
+ }
+
+ fetchAndReplace() {
+ this.fetch({concat: false})
+ }
+
+ fetchAndConcat() {
+ this.fetch({concat: true})
}
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) {
@@ -143,13 +164,13 @@ export default class PropertyBreakdown extends React.Component {
changePropKey(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() {
if (this.state.loading) {
return
- } else if (this.state.moreResultsAvailable) {
+ } else if (this.state.moreResultsAvailable && this.props.query.period !== 'realtime') {
return (
diff --git a/assets/js/dashboard/stats/current-visitors.js b/assets/js/dashboard/stats/current-visitors.js
index b27dfda37..5bfb41144 100644
--- a/assets/js/dashboard/stats/current-visitors.js
+++ b/assets/js/dashboard/stats/current-visitors.js
@@ -8,11 +8,16 @@ export default class CurrentVisitors extends React.Component {
constructor(props) {
super(props)
this.state = {currentVisitors: null}
+ this.updateCount = this.updateCount.bind(this)
}
componentDidMount() {
this.updateCount()
- this.props.timer.onTick(this.updateCount.bind(this))
+ document.addEventListener('tick', this.updateCount)
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('tick', this.updateCount)
}
updateCount() {
diff --git a/assets/js/dashboard/stats/devices/index.js b/assets/js/dashboard/stats/devices/index.js
index 106f5d2a0..c39fd54d9 100644
--- a/assets/js/dashboard/stats/devices/index.js
+++ b/assets/js/dashboard/stats/devices/index.js
@@ -139,18 +139,18 @@ export default class Devices extends React.Component {
switch (this.state.mode) {
case 'browser':
if (this.props.query.filters.browser) {
- return
+ return
}
- return
+ return
case 'os':
if (this.props.query.filters.os) {
- return
+ return
}
- return
+ return
case 'size':
default:
return (
-
+
)
}
}
diff --git a/assets/js/dashboard/stats/graph/graph-util.js b/assets/js/dashboard/stats/graph/graph-util.js
index 98000e69a..cc306cb6a 100644
--- a/assets/js/dashboard/stats/graph/graph-util.js
+++ b/assets/js/dashboard/stats/graph/graph-util.js
@@ -1,8 +1,6 @@
import numberFormatter, {durationFormatter} from '../../util/number-formatter'
import dateFormatter from './date-formatter.js'
-export const INTERVALS = ["month", "week", "date", "hour", "minute"]
-
export const METRIC_MAPPING = {
'Unique visitors (last 30 min)': 'visitors',
'Pageviews (last 30 min)': 'pageviews',
diff --git a/assets/js/dashboard/stats/graph/visitor-graph.js b/assets/js/dashboard/stats/graph/visitor-graph.js
index 8f8e8c40f..fbe61e647 100644
--- a/assets/js/dashboard/stats/graph/visitor-graph.js
+++ b/assets/js/dashboard/stats/graph/visitor-graph.js
@@ -5,7 +5,7 @@ import { navigateToQuery } from '../../query'
import * as api from '../../api'
import * as storage from '../../util/storage'
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 TopStats from './top-stats';
import { IntervalPicker, getStoredInterval, storeInterval } from './interval-picker';
@@ -233,7 +233,7 @@ class LineGraph extends React.Component {
)
} 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 endpoint = `/${encodeURIComponent(this.props.site.domain)}/export${queryParams}`
@@ -314,81 +314,71 @@ export default class VisitorGraph extends React.Component {
this.state = {
topStatsLoadingState: LOADING_STATE.loading,
mainGraphLoadingState: LOADING_STATE.loading,
- metric: storage.getItem(`metric__${this.props.site.domain}`) || 'visitors',
- interval: getStoredInterval(this.props.query.period, this.props.site.domain)
+ metric: storage.getItem(`metric__${this.props.site.domain}`) || 'visitors'
}
this.onVisible = this.onVisible.bind(this)
this.updateMetric = this.updateMetric.bind(this)
this.fetchTopStatData = this.fetchTopStatData.bind(this)
this.fetchGraphData = this.fetchGraphData.bind(this)
- this.maybeRollbackInterval = this.maybeRollbackInterval.bind(this)
this.updateInterval = this.updateInterval.bind(this)
}
- isIntervalValid({ query, site }) {
- const period = query?.period
- const validIntervalsForPeriod = site.validIntervalsByPeriod[period] || []
- const storedInterval = getStoredInterval(period, site.domain)
+ isIntervalValid(interval) {
+ const { query, site } = this.props
+ const validIntervals = site.validIntervalsByPeriod[query.period] || []
- return validIntervalsForPeriod.includes(storedInterval)
+ return validIntervals.includes(interval)
}
- maybeRollbackInterval() {
- if (this.isIntervalValid(this.props)) {
- const interval = getStoredInterval(this.props.query.period, this.props.site.domain)
+ getIntervalFromStorage() {
+ const { query, site } = this.props
+ const storedInterval = getStoredInterval(query.period, site.domain)
- this.setState({interval}, () => {
- this.fetchGraphData()
- })
+ if (this.isIntervalValid(storedInterval)) {
+ return storedInterval
} else {
- this.setState({interval: undefined}, () => {
- this.setState({graphData: null})
- this.fetchGraphData()
- })
+ return null
}
}
updateInterval(interval) {
- if (INTERVALS.includes(interval)) {
- this.setState({interval, mainGraphLoadingState: LOADING_STATE.refreshing}, this.maybeRollbackInterval)
+ if (this.isIntervalValid(interval)) {
storeInterval(this.props.query.period, this.props.site.domain, interval)
+ this.setState({ mainGraphLoadingState: LOADING_STATE.refreshing }, this.fetchGraphData)
}
}
onVisible() {
- this.setState({mainGraphLoadingState: LOADING_STATE.loading}, this.maybeRollbackInterval)
+ this.setState({mainGraphLoadingState: LOADING_STATE.loading}, this.fetchGraphData)
this.fetchTopStatData()
- if (this.props.timer) {
- this.props.timer.onTick(this.maybeRollbackInterval)
- this.props.timer.onTick(this.fetchTopStatData)
+ if (this.props.query.period === 'realtime') {
+ document.addEventListener('tick', this.fetchGraphData)
+ document.addEventListener('tick', this.fetchTopStatData)
}
}
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 (metric) {
- this.setState({ mainGraphLoadingState: LOADING_STATE.loading, topStatsLoadingState: LOADING_STATE.loading, graphData: null, topStatData: null }, this.maybeRollbackInterval)
- } else {
+ if (query !== prevProps.query) {
+ if (this.isGraphCollapsed()) {
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()
}
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) {
- this.setState({mainGraphLoadingState: LOADING_STATE.refreshing}, this.maybeRollbackInterval)
- }
-
- const savedMetric = storage.getItem(`metric__${this.props.site.domain}`)
+ const savedMetric = storage.getItem(`metric__${site.domain}`)
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)
if (topStatLabels && `${topStatLabels}` !== `${prevTopStatLabels}`) {
- if (this.props.query.filters.goal && metric !== 'conversions') {
+ if (query.filters.goal && metric !== 'conversions') {
this.setState({ metric: 'conversions' })
} else if (topStatLabels.includes(savedMetric) && savedMetric !== "") {
this.setState({ metric: savedMetric })
@@ -398,41 +388,48 @@ export default class VisitorGraph extends React.Component {
}
}
- updateMetric(newMetric) {
- if (newMetric === this.state.metric) {
- storage.setItem(`metric__${this.props.site.domain}`, "")
- this.setState({ metric: "" })
- } else {
- storage.setItem(`metric__${this.props.site.domain}`, newMetric)
- this.setState({ metric: newMetric })
- }
+ isGraphCollapsed() {
+ return this.state.metric === ""
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener('tick', this.fetchGraphData)
+ 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() {
- if (!this.state.metric) {
- this.setState({ mainGraphLoadingState: LOADING_STATE.ready, graphData: null })
+ if (this.isGraphCollapsed()) {
+ this.setState({ mainGraphLoadingState: LOADING_STATE.loaded, graphData: null })
return
}
const url = `/api/stats/${encodeURIComponent(this.props.site.domain)}/main-graph`
- let params = {metric: this.state.metric || 'none'}
- if (this.state.interval) { params.interval = this.state.interval }
+ let params = { metric: this.state.metric }
+ const interval = this.getIntervalFromStorage()
+ if (interval) { params.interval = interval }
api.get(url, this.props.query, params)
.then((res) => {
- this.setState({ mainGraphLoadingState: LOADING_STATE.ready, graphData: res })
+ this.setState({ mainGraphLoadingState: LOADING_STATE.loaded, graphData: res })
return res
})
.catch((err) => {
console.log(err)
- this.setState({ mainGraphLoadingState: LOADING_STATE.ready, graphData: false })
+ this.setState({ mainGraphLoadingState: LOADING_STATE.loaded, graphData: false })
})
}
fetchTopStatData() {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/top-stats`, this.props.query)
.then((res) => {
- this.setState({ topStatsLoadingState: LOADING_STATE.ready, topStatData: res })
+ this.setState({ topStatsLoadingState: LOADING_STATE.loaded, topStatData: res })
return res
})
}
@@ -443,14 +440,14 @@ export default class VisitorGraph extends React.Component {
const theme = document.querySelector('html').classList.contains('dark') || false
- const topStatsReadyOrRefreshing = (topStatsLoadingState === LOADING_STATE.ready || topStatsLoadingState === LOADING_STATE.refreshing)
- const mainGraphReadyOrRefreshing = (mainGraphLoadingState === LOADING_STATE.ready || mainGraphLoadingState === LOADING_STATE.refreshing)
+ const topStatsLoadedOrRefreshing = (topStatsLoadingState === LOADING_STATE.loaded || topStatsLoadingState === LOADING_STATE.refreshing)
+ const mainGraphLoadedOrRefreshing = (mainGraphLoadingState === LOADING_STATE.loaded || mainGraphLoadingState === LOADING_STATE.refreshing)
const noMetricOrRefreshing = (!metric || mainGraphLoadingState === LOADING_STATE.refreshing)
const topStatAndGraphLoaded = !!(topStatData && graphData)
const showGraph =
- topStatsReadyOrRefreshing &&
- mainGraphReadyOrRefreshing &&
+ topStatsLoadedOrRefreshing &&
+ mainGraphLoadedOrRefreshing &&
(topStatData && noMetricOrRefreshing || topStatAndGraphLoaded)
return (
diff --git a/assets/js/dashboard/stats/locations/index.js b/assets/js/dashboard/stats/locations/index.js
index 97a7e04c7..0167f9335 100644
--- a/assets/js/dashboard/stats/locations/index.js
+++ b/assets/js/dashboard/stats/locations/index.js
@@ -131,14 +131,14 @@ export default class Locations extends React.Component {
renderContent() {
switch(this.state.mode) {
case "cities":
- return
+ return
case "regions":
- return
+ return
case "countries":
- return
+ return
case "map":
default:
- return
+ return
}
}
diff --git a/assets/js/dashboard/stats/locations/map.js b/assets/js/dashboard/stats/locations/map.js
index ddae98586..5c2f2b333 100644
--- a/assets/js/dashboard/stats/locations/map.js
+++ b/assets/js/dashboard/stats/locations/map.js
@@ -21,6 +21,7 @@ class Countries extends React.Component {
darkTheme: document.querySelector('html').classList.contains('dark') || false
}
this.onVisible = this.onVisible.bind(this)
+ this.updateCountries = this.updateCountries.bind(this)
}
componentDidUpdate(prevProps) {
@@ -33,12 +34,15 @@ class Countries extends React.Component {
componentWillUnmount() {
window.removeEventListener('resize', this.resizeMap);
+ document.removeEventListener('tick', this.updateCountries);
}
onVisible() {
this.fetchCountries().then(this.drawMap.bind(this))
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() {
diff --git a/assets/js/dashboard/stats/pages/index.js b/assets/js/dashboard/stats/pages/index.js
index 4a0cc10f6..c7d051460 100644
--- a/assets/js/dashboard/stats/pages/index.js
+++ b/assets/js/dashboard/stats/pages/index.js
@@ -101,12 +101,12 @@ export default class Pages extends React.Component {
renderContent() {
switch(this.state.mode) {
case "entry-pages":
- return
+ return
case "exit-pages":
- return
+ return
case "pages":
default:
- return
+ return
}
}
diff --git a/assets/js/dashboard/stats/reports/list.js b/assets/js/dashboard/stats/reports/list.js
index 7a8cec529..b838e163f 100644
--- a/assets/js/dashboard/stats/reports/list.js
+++ b/assets/js/dashboard/stats/reports/list.js
@@ -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 FlipMove from 'react-flip-move';
+
import FadeIn from '../../fade-in'
import MoreLink from '../more-link'
@@ -28,25 +30,32 @@ function ExternalLink({item, externalLinkDest}) {
export default function ListReport(props) {
const [state, setState] = useState({loading: true, list: null})
+ const [visible, setVisible] = useState(false)
const valueKey = props.valueKey || 'visitors'
const showConversionRate = !!props.query.filters.goal
- const prevQuery = useRef();
- function fetchData() {
- if (typeof(prevQuery.current) === 'undefined' || prevQuery.current !== props.query) {
- prevQuery.current = props.query;
- setState({loading: true, list: null})
+ const fetchData = useCallback(() => {
+ if (props.query.period !== 'realtime') {
+ setState({loading: true, list: null})
+ }
props.fetchData()
.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() {
- fetchData()
- if (props.timer) props.timer.onTick(fetchData)
- }
-
+ useEffect(() => {
+ return () => { document.removeEventListener('tick', fetchData) }
+ }, []);
function label() {
if (props.query.period === 'realtime') {
@@ -60,8 +69,6 @@ export default function ListReport(props) {
return props.valueLabel || 'Visitors'
}
- useEffect(fetchData, [props.query]);
-
function renderListItem(listItem) {
const query = new URLSearchParams(window.location.search)
@@ -115,7 +122,9 @@ export default function ListReport(props) {
{showConversionRate && CR }
- { state.list && state.list.map(renderListItem) }
+
+ { state.list.map(renderListItem) }
+
>
)
}
diff --git a/assets/js/dashboard/stats/sources/referrer-list.js b/assets/js/dashboard/stats/sources/referrer-list.js
index e4918562b..d526f44f8 100644
--- a/assets/js/dashboard/stats/sources/referrer-list.js
+++ b/assets/js/dashboard/stats/sources/referrer-list.js
@@ -23,11 +23,14 @@ export default class Referrers extends React.Component {
super(props)
this.state = { loading: true }
this.onVisible = this.onVisible.bind(this)
+ this.fetchReferrers = this.fetchReferrers.bind(this)
}
onVisible() {
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) {
@@ -37,6 +40,10 @@ export default class Referrers extends React.Component {
}
}
+ componentWillUnmount() {
+ document.removeEventListener('tick', this.fetchReferrers)
+ }
+
showConversionRate() {
return !!this.props.query.filters.goal
}
diff --git a/assets/js/dashboard/stats/sources/search-terms.js b/assets/js/dashboard/stats/sources/search-terms.js
index c23710739..cd0c3b60c 100644
--- a/assets/js/dashboard/stats/sources/search-terms.js
+++ b/assets/js/dashboard/stats/sources/search-terms.js
@@ -5,15 +5,21 @@ import MoreLink from '../more-link'
import numberFormatter from '../../util/number-formatter'
import RocketIcon from '../modals/rocket-icon'
import * as api from '../../api'
+import LazyLoader from '../../components/lazy-loader'
export default class SearchTerms extends React.Component {
constructor(props) {
super(props)
this.state = {loading: true}
+ this.onVisible = this.onVisible.bind(this)
+ this.fetchSearchTerms = this.fetchSearchTerms.bind(this)
}
- componentDidMount() {
+ onVisible() {
this.fetchSearchTerms()
+ if (this.props.query.period === 'realtime') {
+ document.addEventListener('tick', this.fetchSearchTerms)
+ }
}
componentDidUpdate(prevProps) {
@@ -23,6 +29,10 @@ export default class SearchTerms extends React.Component {
}
}
+ componentWillUnmount() {
+ document.removeEventListener('tick', this.fetchSearchTerms)
+ }
+
fetchSearchTerms() {
api.get(`/api/stats/${encodeURIComponent(this.props.site.domain)}/referrers/Google`, this.props.query)
.then((res) => this.setState({
@@ -30,7 +40,7 @@ export default class SearchTerms extends React.Component {
searchTerms: res.search_terms || [],
notConfigured: res.not_configured,
isAdmin: res.is_admin
- })).catch((error) =>
+ })).catch((error) =>
{
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 && }
- { this.renderContent() }
+
+ { this.renderContent() }
+
)
diff --git a/assets/js/dashboard/stats/sources/source-list.js b/assets/js/dashboard/stats/sources/source-list.js
index ff5ce7c5f..708534834 100644
--- a/assets/js/dashboard/stats/sources/source-list.js
+++ b/assets/js/dashboard/stats/sources/source-list.js
@@ -15,12 +15,15 @@ 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.timer) this.props.timer.onTick(this.fetchReferrers.bind(this))
+ if (this.props.query.period === 'realtime') {
+ document.addEventListener('tick', this.fetchReferrers)
+ }
}
componentDidUpdate(prevProps) {
@@ -30,6 +33,10 @@ class AllSources extends React.Component {
}
}
+ componentWillUnmount() {
+ document.removeEventListener('tick', this.fetchReferrers)
+ }
+
showConversionRate() {
return !!this.props.query.filters.goal
}
@@ -103,7 +110,7 @@ class AllSources extends React.Component {
)
} else {
- return No data yet
+ return No data yet
}
}
@@ -144,12 +151,16 @@ const UTM_TAGS = {
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 }
}
- componentDidMount() {
+ onVisible() {
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) {
@@ -159,6 +170,10 @@ class UTMSources extends React.Component {
}
}
+ componentWillUnmount() {
+ document.removeEventListener('tick', this.fetchReferrers)
+ }
+
showNoRef() {
return this.props.query.period === 'realtime'
}
@@ -240,7 +255,7 @@ class UTMSources extends React.Component {
renderContent() {
return (
-
+
Top Sources
{this.props.renderTabs()}
@@ -249,7 +264,7 @@ class UTMSources extends React.Component {
{this.renderList()}
-
+
)
}
@@ -289,7 +304,7 @@ export default class SourceList extends React.Component {
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 = ['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'
return (
@@ -344,15 +359,7 @@ export default class SourceList extends React.Component {
render() {
if (this.state.tab === 'all') {
return
- } else if (this.state.tab === 'utm_medium') {
- return
- } else if (this.state.tab === 'utm_source') {
- return
- } else if (this.state.tab === 'utm_campaign') {
- return
- } else if (this.state.tab === 'utm_content') {
- return
- } else if (this.state.tab === 'utm_term') {
+ } else if (Object.keys(UTM_TAGS).includes(this.state.tab)) {
return
}
}
diff --git a/assets/js/dashboard/util/realtime-update-timer.js b/assets/js/dashboard/util/realtime-update-timer.js
new file mode 100644
index 000000000..77a4f0d0f
--- /dev/null
+++ b/assets/js/dashboard/util/realtime-update-timer.js
@@ -0,0 +1,8 @@
+const THIRTY_SECONDS = 30000
+const tickEvent = new Event('tick')
+
+export function start() {
+ setInterval(() => {
+ document.dispatchEvent(tickEvent)
+ }, THIRTY_SECONDS)
+}