Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter imported data #4104

Closed
wants to merge 11 commits into from
Closed
1 change: 1 addition & 0 deletions assets/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ if (container) {
revenueGoals: JSON.parse(container.dataset.revenueGoals),
funnels: JSON.parse(container.dataset.funnels),
statsBegin: container.dataset.statsBegin,
hasImportedData: container.dataset.hasImportedData === 'true',
nativeStatsBegin: container.dataset.nativeStatsBegin,
embedded: container.dataset.embedded,
background: container.dataset.background,
Expand Down
11 changes: 10 additions & 1 deletion assets/js/dashboard/stats/behaviours/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import GoalConversions, { specialTitleWhenGoalFilter } from './goal-conversions'
import Properties from './props'
import { FeatureSetupNotice } from '../../components/notice'
import { SPECIAL_GOALS } from './goal-conversions'
import ImportedQueryValidationBoundary from '../imported-query-validation-boundary'

/*global BUILD_EXTRA*/
/*global require*/
Expand Down Expand Up @@ -48,6 +49,10 @@ export default function Behaviours(props) {
const [showingPropsForGoalFilter, setShowingPropsForGoalFilter] = useState(false)

const onGoalFilterClick = useCallback((e) => {
if (query.with_imported && site.hasImportedData) {
return
}

const goalName = e.target.innerHTML
const isSpecialGoal = Object.keys(SPECIAL_GOALS).includes(goalName)
const isPageviewGoal = goalName.startsWith('Visit ')
Expand Down Expand Up @@ -171,7 +176,11 @@ export default function Behaviours(props) {

function renderConversions() {
if (site.hasGoals) {
return <GoalConversions site={site} query={query} onGoalFilterClick={onGoalFilterClick} />
return (
<ImportedQueryValidationBoundary property={'goal'} query={query} classNames={"max-w-lg mx-auto mt-6 mb-16"}>
<GoalConversions site={site} query={query} onGoalFilterClick={onGoalFilterClick} />
</ImportedQueryValidationBoundary>
)
}
else if (adminAccess) {
return (
Expand Down
35 changes: 27 additions & 8 deletions assets/js/dashboard/stats/devices/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import * as api from '../../api'
import * as url from '../../util/url'
import { VISITORS_METRIC, PERCENTAGE_METRIC, maybeWithCR } from '../reports/metrics';
import ImportedQueryValidationBoundary from '../imported-query-validation-boundary';

function Browsers({ query, site }) {
function fetchData() {
Expand Down Expand Up @@ -142,6 +143,12 @@
}
}

MODE_TO_PROPERTY_MAPPING = {

Check failure on line 146 in assets/js/dashboard/stats/devices/index.js

View workflow job for this annotation

GitHub Actions / Build and test

'MODE_TO_PROPERTY_MAPPING' is not defined
'browser': 'browser',
'os': 'os',
'size': 'screen'
}

export default class Devices extends React.Component {
constructor(props) {
super(props)
Expand All @@ -160,21 +167,27 @@
}

renderContent() {
const {site, query} = this.props

const filtersByBrowser = isFilteringOnFixedValue(query, 'browser')
const filtersByOS = isFilteringOnFixedValue(query, 'os')
const includesImported = query.with_imported && site.hasImportedData

switch (this.state.mode) {
case 'browser':
if (isFilteringOnFixedValue(this.props.query, 'browser')) {
return <BrowserVersions site={this.props.site} query={this.props.query} />
if (filtersByBrowser && !includesImported) {
return <BrowserVersions site={site} query={query} />
}
return <Browsers site={this.props.site} query={this.props.query} />
return <Browsers site={site} query={query} />
case 'os':
if (isFilteringOnFixedValue(this.props.query, 'os')) {
return <OperatingSystemVersions site={this.props.site} query={this.props.query} />
if (filtersByOS && !includesImported) {
return <OperatingSystemVersions site={site} query={query} />
}
return <OperatingSystems site={this.props.site} query={this.props.query} />
return <OperatingSystems site={site} query={query} />
case 'size':
default:
return (
<ScreenSizes site={this.props.site} query={this.props.query} />
<ScreenSizes site={site} query={query} />
)
}
}
Expand Down Expand Up @@ -213,7 +226,13 @@
{this.renderPill('Size', 'size')}
</div>
</div>
{this.renderContent()}
<ImportedQueryValidationBoundary
property={MODE_TO_PROPERTY_MAPPING[this.state.mode]}

Check failure on line 230 in assets/js/dashboard/stats/devices/index.js

View workflow job for this annotation

GitHub Actions / Build and test

'MODE_TO_PROPERTY_MAPPING' is not defined
query={this.props.query}
classNames={"mt-20"}
>
{this.renderContent()}
</ImportedQueryValidationBoundary>
</div>
)
}
Expand Down
61 changes: 61 additions & 0 deletions assets/js/dashboard/stats/imported-query-validation-boundary.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react'
import RocketIcon from './modals/rocket-icon'
import { BarsArrowUpIcon } from '@heroicons/react/20/solid'

/*
This function checks whether the current set of query filters supports
imported data. It expects a `children` prop with any report contents
which will be rendered if the conditions are satisfied.

Given that `query.with_imported` is false, the children will simply be
rendered. Otherwise, the filters (and the breakdown property) will be
checked against the following rules:

1) Special custom props `url` and `path` require their corresponding
special goal filter

2) Only a single property can be filtered by

3) If a breakdown property is given (i.e. it's not an aggregate or
timeseries query), then it has to match with the property that is
filtered by. Only exception is the `url` or `path` breakdown, in
which case a goal filter is *required*

If any of these rules is violated, an error message will be displayed
to the user, instead of the actual report contents.
*/
export default function ImportedQueryValidationBoundary({property, query, children, classNames}) {
console.log(query.filters)
const propsInFilter = Object.keys(query.filters)
.filter(filter_key => query.filters[filter_key])

let isSupportedFilterSet

if (!query.with_imported) {
isSupportedFilterSet = true
}
else if (propsInFilter.length === 0) {
isSupportedFilterSet = true
}
else if (propsInFilter.length === 1) {
isSupportedFilterSet = property === propsInFilter[0]
}
else {
isSupportedFilterSet = false
}

if (isSupportedFilterSet) {
return children
} else {
return (
<div className={`text-center text-gray-700 dark:text-gray-300 text-sm ${classNames}`}>
<RocketIcon />
<p>
We're unable to show this report based on imported data with the current set of filters. Find the

Check failure on line 54 in assets/js/dashboard/stats/imported-query-validation-boundary.js

View workflow job for this annotation

GitHub Actions / Build and test

`'` can be escaped with `&apos;`, `&lsquo;`, `&#39;`, `&rsquo;`
<BarsArrowUpIcon className={"mx-1 inline w-5 h-5 dark:text-gray-300 text-gray-700"} />
icon above the graph to switch to native data only.
</p>
</div>
)
}
}
32 changes: 25 additions & 7 deletions assets/js/dashboard/stats/locations/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import * as api from '../../api'
import {apiPath, sitePath} from '../../util/url'
import ListReport from '../reports/list'
import { VISITORS_METRIC, maybeWithCR } from '../reports/metrics';
import ImportedQueryValidationBoundary from '../imported-query-validation-boundary';

function Countries({query, site, onClick}) {
function fetchData() {
Expand Down Expand Up @@ -94,10 +95,11 @@ function Cities({query, site}) {
}


const labelFor = {
'countries': 'Countries',
'regions': 'Regions',
'cities': 'Cities',
const TABS = {
'map': {label: 'Locations', property: 'country'},
'countries': {label: 'Countries', property: 'country'},
'regions': {label: 'Regions', property: 'region'},
'cities': {label: 'Cities', property: 'city'},
}

export default class Locations extends React.Component {
Expand All @@ -112,6 +114,11 @@ export default class Locations extends React.Component {
}
}

includesImported() {
const {site, query} = this.props
return query.with_imported && site.hasImportedData
}

componentDidUpdate(prevProps) {
const isRemovingFilter = (filterName) => {
return prevProps.query.filters[filterName] && !this.props.query.filters[filterName]
Expand All @@ -134,14 +141,19 @@ export default class Locations extends React.Component {
}

onCountryFilter(mode) {
if (this.includesImported()) {
return () => {}
}
return () => {
this.countriesRestoreMode = mode
this.setMode('regions')()
}
}

onRegionFilter() {
this.setMode('cities')()
if (!this.includesImported()) {
this.setMode('cities')()
}
}

renderContent() {
Expand Down Expand Up @@ -186,7 +198,7 @@ export default class Locations extends React.Component {
<div>
<div className="w-full flex justify-between">
<h3 className="font-bold dark:text-gray-100">
{labelFor[this.state.mode] || 'Locations'}
{TABS[this.state.mode].label || 'Locations'}
</h3>
<div className="flex text-xs font-medium text-gray-500 dark:text-gray-400 space-x-2">
{ this.renderPill('Map', 'map') }
Expand All @@ -195,7 +207,13 @@ export default class Locations extends React.Component {
{ this.renderPill('Cities', 'cities') }
</div>
</div>
{this.renderContent()}
<ImportedQueryValidationBoundary
property={TABS[this.state.mode].property}
query={this.props.query}
classNames={"mt-20"}
>
{this.renderContent()}
</ImportedQueryValidationBoundary>
</div>
)
}
Expand Down
19 changes: 13 additions & 6 deletions assets/js/dashboard/stats/pages/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as url from '../../util/url'
import * as api from '../../api'
import ListReport from './../reports/list'
import { VISITORS_METRIC, maybeWithCR } from './../reports/metrics';
import ImportedQueryValidationBoundary from './../imported-query-validation-boundary'

function EntryPages({ query, site }) {
function fetchData() {
Expand Down Expand Up @@ -87,10 +88,10 @@ function TopPages({ query, site }) {
)
}

const labelFor = {
'pages': 'Top Pages',
'entry-pages': 'Entry Pages',
'exit-pages': 'Exit Pages',
const TABS = {
'pages': {label: 'Top Pages', property: 'page'},
'entry-pages': {label: 'Entry Pages', property: 'entry_page'},
'exit-pages': {label: 'Exit Pages', property: 'exit_page'}
}

export default class Pages extends React.Component {
Expand Down Expand Up @@ -152,7 +153,7 @@ export default class Pages extends React.Component {
{/* Header Container */}
<div className="w-full flex justify-between">
<h3 className="font-bold dark:text-gray-100">
{labelFor[this.state.mode] || 'Page Visits'}
{TABS[this.state.mode].label || 'Page Visits'}
</h3>
<div className="flex font-medium text-xs text-gray-500 dark:text-gray-400 space-x-2">
{this.renderPill('Top Pages', 'pages')}
Expand All @@ -161,7 +162,13 @@ export default class Pages extends React.Component {
</div>
</div>
{/* Main Contents */}
{this.renderContent()}
<ImportedQueryValidationBoundary
property={TABS[this.state.mode].property}
query={this.props.query}
classNames={"mt-20"}
>
{this.renderContent()}
</ImportedQueryValidationBoundary>
</div>
)
}
Expand Down
7 changes: 6 additions & 1 deletion assets/js/dashboard/stats/sources/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ import { isFilteringOnFixedValue } from '../../util/filters'


export default function Sources(props) {
const { site, query } = props

const filtersBySource = isFilteringOnFixedValue(query, 'source')
const includesImported = query.with_imported && site.hasImportedData

if (props.query.filters.source === 'Google') {
return <SearchTerms {...props} />
} else if (isFilteringOnFixedValue(props.query, 'source')) {
} else if (filtersBySource && !includesImported) {
return <ReferrerList {...props} />
} else {
return <SourceList {...props} />
Expand Down
53 changes: 29 additions & 24 deletions assets/js/dashboard/stats/sources/source-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ import { Menu, Transition } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { escapeFilterValue } from '../../util/filters'
import classNames from 'classnames'
import ImportedQueryValidationBoundary from './../imported-query-validation-boundary'

const UTM_TAGS = {
utm_medium: { label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: '/utm_mediums' },
utm_source: { label: 'UTM Source', shortLabel: 'UTM Source', endpoint: '/utm_sources' },
utm_campaign: { label: 'UTM Campaign', shortLabel: 'UTM Campai', endpoint: '/utm_campaigns' },
utm_content: { label: 'UTM Content', shortLabel: 'UTM Conten', endpoint: '/utm_contents' },
utm_term: { label: 'UTM Term', shortLabel: 'UTM Term', endpoint: '/utm_terms' },
utm_medium: { label: 'UTM Medium', shortLabel: 'UTM Medium', endpoint: '/utm_mediums', property: 'utm_medium' },
utm_source: { label: 'UTM Source', shortLabel: 'UTM Source', endpoint: '/utm_sources', property: 'utm_source' },
utm_campaign: { label: 'UTM Campaign', shortLabel: 'UTM Campai', endpoint: '/utm_campaigns', property: 'utm_campaign' },
utm_content: { label: 'UTM Content', shortLabel: 'UTM Conten', endpoint: '/utm_contents', property: 'utm_content' },
utm_term: { label: 'UTM Term', shortLabel: 'UTM Term', endpoint: '/utm_terms', property: 'utm_term' },
}

function AllSources(props) {
Expand All @@ -39,16 +40,18 @@ function AllSources(props) {
}

return (
<ListReport
fetchData={fetchData}
getFilterFor={getFilterFor}
keyLabel="Source"
metrics={maybeWithCR([VISITORS_METRIC], query)}
detailsLink={url.sitePath(site, '/sources')}
renderIcon={renderIcon}
query={query}
color="bg-blue-50"
/>
<ImportedQueryValidationBoundary property={'source'} query={query} classNames={"mt-20"}>
<ListReport
fetchData={fetchData}
getFilterFor={getFilterFor}
keyLabel="Source"
metrics={maybeWithCR([VISITORS_METRIC], query)}
detailsLink={url.sitePath(site, '/sources')}
renderIcon={renderIcon}
query={query}
color="bg-blue-50"
/>
</ImportedQueryValidationBoundary>
)
}

Expand All @@ -65,15 +68,17 @@ function UTMSources(props) {
}

return (
<ListReport
fetchData={fetchData}
getFilterFor={getFilterFor}
keyLabel={utmTag.label}
metrics={maybeWithCR([VISITORS_METRIC], query)}
detailsLink={url.sitePath(site, utmTag.endpoint)}
query={query}
color="bg-blue-50"
/>
<ImportedQueryValidationBoundary property={utmTag.property} query={query} classNames={"mt-20"}>
<ListReport
fetchData={fetchData}
getFilterFor={getFilterFor}
keyLabel={utmTag.label}
metrics={maybeWithCR([VISITORS_METRIC], query)}
detailsLink={url.sitePath(site, utmTag.endpoint)}
query={query}
color="bg-blue-50"
/>
</ImportedQueryValidationBoundary>
)
}

Expand Down
Loading
Loading