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 (
) } 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) +}