Disable parts of SchemaExplorer in CEO if user-defined temp vars in query

Use constants for :dashboardTime:, :upperDashboardTime:, & :interval:.
Clarify some fn names.

Co-authored-by: Iris Scholten <ischolten.is@gmail.com>
pull/10616/head
Iris Scholten 2018-04-27 15:22:01 -07:00
parent a65b5603fb
commit c288e500c6
26 changed files with 186 additions and 38 deletions

View File

@ -14,7 +14,7 @@ import * as queryModifiers from 'src/utils/queryTransitions'
import defaultQueryConfig from 'src/utils/defaultQueryConfig'
import {buildQuery} from 'src/utils/influxql'
import {getQueryConfig} from 'src/shared/apis'
import {getQueryConfigAndStatus} from 'src/shared/apis'
import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {ColorString, ColorNumber} from 'src/types/colors'
import {nextSource} from 'src/dashboards/utils/sources'
@ -25,7 +25,11 @@ import {
} from 'src/dashboards/constants'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {AUTO_GROUP_BY} from 'src/shared/constants'
import {
AUTO_GROUP_BY,
PREDEFINED_TEMP_VARS,
TEMP_VAR_DASHBOARD_TIME,
} from 'src/shared/constants'
import {getCellTypeColors} from 'src/dashboards/constants/cellEditor'
import {TimeRange, Source, Query} from 'src/types'
import {Status} from 'src/types/query'
@ -270,7 +274,7 @@ class CellEditorOverlay extends Component<Props, State> {
const {cell, thresholdsListColors, gaugeColors, lineColors} = this.props
const queries = queriesWorkingDraft.map(q => {
const timeRange = q.range || {upper: null, lower: ':dashboardTime:'}
const timeRange = q.range || {upper: null, lower: TEMP_VAR_DASHBOARD_TIME}
return {
queryConfig: q,
@ -321,16 +325,53 @@ class CellEditorOverlay extends Component<Props, State> {
return _.get(queriesWorkingDraft, activeQueryIndex, queriesWorkingDraft[0])
}
// The schema explorer is not built to handle user defined template variables
// in the query in a clear manner. If they are being used, we indicate that in
// the query config in order to disable the fields column down stream because
// at this point the query string is disconnected from the schema explorer.
private handleEditRawText = async (url, id, text) => {
const templates = removeUnselectedTemplateValues(this.props.templates)
// use this as the handler passed into fetchTimeSeries to update a query status
try {
const {data} = await getQueryConfig(url, [{query: text, id}], templates)
const config = data.queries.find(q => q.id === id)
const nextQueries = this.state.queriesWorkingDraft.map(
q => (q.id === id ? {...config.queryConfig, source: q.source} : q)
const userDefinedTempVarsInQuery = this.props.templates.filter(temp => {
const isPredefinedTempVar = !!PREDEFINED_TEMP_VARS.find(
t => t === temp.tempVar
)
if (!isPredefinedTempVar) {
return text.includes(temp.tempVar)
}
return false
})
const isUsingUserDefinedTempVars = !!userDefinedTempVarsInQuery.length
try {
const selectedTempVars = isUsingUserDefinedTempVars
? removeUnselectedTemplateValues(userDefinedTempVarsInQuery)
: []
const {data} = await getQueryConfigAndStatus(
url,
[{query: text, id}],
selectedTempVars
)
const config = data.queries.find(q => q.id === id)
const nextQueries = this.state.queriesWorkingDraft.map(q => {
if (q.id === id) {
const isQuerySupportedByExplorer = !isUsingUserDefinedTempVars
if (isUsingUserDefinedTempVars) {
return {...q, rawText: text, isQuerySupportedByExplorer}
}
return {
...config.queryConfig,
source: q.source,
isQuerySupportedByExplorer,
}
}
return q
})
this.setState({queriesWorkingDraft: nextQueries})
} catch (error) {
console.error(error)

View File

@ -1,5 +1,6 @@
import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import EmptyQuery from 'src/shared/components/EmptyQuery'
import QueryTabList from 'src/shared/components/QueryTabList'
@ -7,8 +8,9 @@ import QueryTextArea from 'src/dashboards/components/QueryTextArea'
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import {buildQuery} from 'utils/influxql'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
const TEMPLATE_RANGE = {upper: null, lower: ':dashboardTime:'}
const TEMPLATE_RANGE = {upper: null, lower: TEMP_VAR_DASHBOARD_TIME}
const rawTextBinder = (links, id, action) => text =>
action(links.queries, id, text)
const buildText = q =>
@ -54,6 +56,11 @@ const QueryMaker = ({
query={activeQuery}
onAddQuery={onAddQuery}
initialGroupByTime={initialGroupByTime}
isQuerySupportedByExplorer={_.get(
activeQuery,
'isQuerySupportedByExplorer',
true
)}
/>
</div>
) : (

View File

@ -36,7 +36,12 @@ import {
templateControlBarVisibilityToggled as templateControlBarVisibilityToggledAction,
} from 'shared/actions/app'
import {presentationButtonDispatcher} from 'shared/dispatchers'
import {interval, DASHBOARD_LAYOUT_ROW_HEIGHT} from 'shared/constants'
import {
interval,
DASHBOARD_LAYOUT_ROW_HEIGHT,
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'shared/constants'
import {notifyDashboardNotFound} from 'shared/copy/notifications'
import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -321,7 +326,7 @@ class DashboardPage extends Component {
const dashboardTime = {
id: 'dashtime',
tempVar: ':dashboardTime:',
tempVar: TEMP_VAR_DASHBOARD_TIME,
type: lowerType,
values: [
{
@ -334,7 +339,7 @@ class DashboardPage extends Component {
const upperDashboardTime = {
id: 'upperdashtime',
tempVar: ':upperDashboardTime:',
tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME,
type: upperType,
values: [
{

View File

@ -1,6 +1,6 @@
import uuid from 'uuid'
import {getQueryConfig} from 'shared/apis'
import {getQueryConfigAndStatus} from 'shared/apis'
import {errorThrown} from 'shared/actions/errors'
@ -158,7 +158,7 @@ export const timeShift = (queryID, shift) => ({
// Async actions
export const editRawTextAsync = (url, id, text) => async dispatch => {
try {
const {data} = await getQueryConfig(url, [{query: text, id}])
const {data} = await getQueryConfigAndStatus(url, [{query: text, id}])
const config = data.queries.find(q => q.id === id)
dispatch(updateQueryConfig(config.queryConfig))
} catch (error) {

View File

@ -20,6 +20,11 @@ class FieldListItem extends Component {
if (e) {
e.stopPropagation()
}
const {isDisabled} = this.props
if (isDisabled) {
return
}
this.setState({isOpen: !this.state.isOpen})
}
@ -139,5 +144,6 @@ FieldListItem.propTypes = {
onApplyFuncsToField: func.isRequired,
isKapacitorRule: bool.isRequired,
funcs: arrayOf(string.isRequired).isRequired,
isDisabled: bool,
}
export default FieldListItem

View File

@ -19,6 +19,7 @@ const GroupByTimeDropdown = ({
selected,
onChooseGroupByTime,
location: {pathname},
isDisabled,
}) => (
<div className="group-by-time">
<label className="group-by-time--label">Group by:</label>
@ -32,11 +33,12 @@ const GroupByTimeDropdown = ({
}))}
onChoose={onChooseGroupByTime}
selected={selected || 'Time'}
disabled={isDisabled}
/>
</div>
)
const {func, string, shape} = PropTypes
const {bool, func, string, shape} = PropTypes
GroupByTimeDropdown.propTypes = {
location: shape({
@ -44,6 +46,7 @@ GroupByTimeDropdown.propTypes = {
}).isRequired,
selected: string,
onChooseGroupByTime: func.isRequired,
isDisabled: bool,
}
export default withRouter(GroupByTimeDropdown)

View File

@ -1,5 +1,7 @@
import {TEMP_VAR_INTERVAL} from 'src/shared/constants'
const groupByTimes = [
{defaultTimeBound: ':interval:', seconds: 604800, menuOption: 'auto'},
{defaultTimeBound: TEMP_VAR_INTERVAL, seconds: 604800, menuOption: 'auto'},
{defaultTimeBound: 'now() - 5m', seconds: 10, menuOption: '10s'},
{defaultTimeBound: 'now() - 15m', seconds: 60, menuOption: '1m'},
{defaultTimeBound: 'now() - 1h', seconds: 300, menuOption: '5m'},

View File

@ -231,7 +231,7 @@ export function kapacitorProxy(kapacitor, method, path, body) {
})
}
export const getQueryConfig = (url, queries, tempVars) =>
export const getQueryConfigAndStatus = (url, queries, tempVars) =>
AJAX({
url,
method: 'POST',

View File

@ -1,7 +1,7 @@
import React, {Component, ComponentClass} from 'react'
import _ from 'lodash'
import {getQueryConfig} from 'src/shared/apis'
import {getQueryConfigAndStatus} from 'src/shared/apis'
import {fetchTimeSeries} from 'src/shared/apis/query'
import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series'
import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series'
@ -265,7 +265,11 @@ const AutoRefresh = (
const host = _.isArray(q.host) ? q.host[0] : q.host
const url = host.replace('proxy', 'queries')
const text = q.text
const {data} = await getQueryConfig(url, [{query: text}], templates)
const {data} = await getQueryConfigAndStatus(
url,
[{query: text}],
templates
)
return data.queries[0].queryAST
})
)

View File

@ -2,8 +2,10 @@ import React from 'react'
import PropTypes from 'prop-types'
import _ from 'lodash'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
const CustomTimeIndicator = ({queries}) => {
const q = queries.find(({query}) => !query.includes(':dashboardTime:'))
const q = queries.find(({query}) => !query.includes(TEMP_VAR_DASHBOARD_TIME))
const customLower = _.get(q, ['queryConfig', 'range', 'lower'], null)
const customUpper = _.get(q, ['queryConfig', 'range', 'upper'], null)

View File

@ -73,8 +73,12 @@ class FieldList extends Component {
addInitialField,
initialGroupByTime: time,
isKapacitorRule,
isQuerySupportedByExplorer,
} = this.props
const {fields, groupBy} = query
if (!isQuerySupportedByExplorer) {
return
}
const initialGroupBy = {...groupBy, time}
if (!_.size(fields)) {
@ -141,6 +145,7 @@ class FieldList extends Component {
render() {
const {
query: {database, measurement, fields = [], groupBy, fill, shifts},
isQuerySupportedByExplorer,
isKapacitorRule,
} = this.props
@ -160,6 +165,7 @@ class FieldList extends Component {
isKapacitorRule={isKapacitorRule}
onTimeShift={this.handleTimeShift}
onGroupByTime={this.handleGroupByTime}
isDisabled={!isQuerySupportedByExplorer}
/>
) : null}
</div>
@ -192,6 +198,7 @@ class FieldList extends Component {
fieldFuncs={fieldFuncs}
funcs={functionNames(funcs)}
isKapacitorRule={isKapacitorRule}
isDisabled={!isQuerySupportedByExplorer}
/>
)
})}
@ -245,6 +252,7 @@ FieldList.propTypes = {
removeFuncs: func.isRequired,
addInitialField: func,
initialGroupByTime: string,
isQuerySupportedByExplorer: bool,
}
export default FieldList

View File

@ -86,7 +86,7 @@ class FillQuery extends Component {
}
render() {
const {size, theme} = this.props
const {size, theme, isDisabled} = this.props
const {selected, currentNumberValue} = this.state
return (
@ -114,6 +114,7 @@ class FillQuery extends Component {
buttonColor="btn-info"
menuClass={`dropdown-${this.getColor(theme)}`}
onChoose={this.handleDropdown}
disabled={isDisabled}
/>
<label className="fill-query--label">Fill:</label>
</div>
@ -121,7 +122,7 @@ class FillQuery extends Component {
}
}
const {func, string} = PropTypes
const {bool, func, string} = PropTypes
FillQuery.defaultProps = {
size: 'sm',
@ -134,6 +135,7 @@ FillQuery.propTypes = {
value: string,
size: string,
theme: string,
isDisabled: bool,
}
export default FillQuery

View File

@ -19,6 +19,7 @@ interface Props {
onChooseTag: () => void
onGroupByTag: () => void
onToggleTagAcceptance: () => void
isQuerySupportedByExplorer: boolean
onChooseMeasurement: (measurement: string) => void
}
@ -117,7 +118,13 @@ class MeasurementList extends PureComponent<Props, State> {
}
public render() {
const {query, querySource, onChooseTag, onGroupByTag} = this.props
const {
query,
querySource,
onChooseTag,
onGroupByTag,
isQuerySupportedByExplorer,
} = this.props
const {database, areTagsAccepted} = query
const {filtered} = this.state
@ -147,6 +154,7 @@ class MeasurementList extends PureComponent<Props, State> {
areTagsAccepted={areTagsAccepted}
onAcceptReject={this.handleAcceptReject}
isActive={measurement === query.measurement}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
numTagsActive={Object.keys(query.tags).length}
onChooseMeasurement={this.handleChoosemeasurement}
/>

View File

@ -38,6 +38,7 @@ interface Props {
onChooseTag: () => void
onGroupByTag: () => void
onAcceptReject: () => void
isQuerySupportedByExplorer: boolean
onChooseMeasurement: (measurement: string) => () => void
}
@ -62,6 +63,7 @@ class MeasurementListItem extends PureComponent<Props, State> {
onGroupByTag,
numTagsActive,
areTagsAccepted,
isQuerySupportedByExplorer,
} = this.props
return (
@ -96,6 +98,7 @@ class MeasurementListItem extends PureComponent<Props, State> {
querySource={querySource}
onChooseTag={onChooseTag}
onGroupByTag={onGroupByTag}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/>
)}
</div>
@ -105,6 +108,11 @@ class MeasurementListItem extends PureComponent<Props, State> {
private handleAcceptReject = (e: MouseEvent<HTMLElement>) => {
e.stopPropagation()
const {isQuerySupportedByExplorer} = this.props
if (!isQuerySupportedByExplorer) {
return
}
const {onAcceptReject} = this.props
onAcceptReject()
}

View File

@ -12,19 +12,24 @@ const QueryOptions = ({
onTimeShift,
onGroupByTime,
isKapacitorRule,
isDisabled,
}) => (
<div className="query-builder--groupby-fill-container">
<GroupByTimeDropdown
selected={groupBy.time}
onChooseGroupByTime={onGroupByTime}
isDisabled={isDisabled}
/>
{isKapacitorRule ? null : (
<TimeShiftDropdown
selected={shift && shift.label}
onChooseTimeShift={onTimeShift}
isDisabled={isDisabled}
/>
)}
{isKapacitorRule ? null : <FillQuery value={fill} onChooseFill={onFill} />}
{isKapacitorRule ? null : (
<FillQuery value={fill} onChooseFill={onFill} isDisabled={isDisabled} />
)}
</div>
)
@ -42,6 +47,7 @@ QueryOptions.propTypes = {
onGroupByTime: func.isRequired,
isKapacitorRule: bool.isRequired,
onTimeShift: func.isRequired,
isDisabled: bool,
}
export default QueryOptions

View File

@ -12,6 +12,7 @@ const SchemaExplorer = ({
query: {id},
source,
initialGroupByTime,
isQuerySupportedByExplorer,
actions: {
fill,
timeShift,
@ -41,6 +42,7 @@ const SchemaExplorer = ({
onGroupByTag={actionBinder(id, groupByTag)}
onChooseMeasurement={actionBinder(id, chooseMeasurement)}
onToggleTagAcceptance={actionBinder(id, toggleTagAcceptance)}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/>
<FieldList
source={source}
@ -54,11 +56,16 @@ const SchemaExplorer = ({
onGroupByTime={actionBinder(id, groupByTime)}
addInitialField={actionBinder(id, addInitialField)}
applyFuncsToField={actionBinder(id, applyFuncsToField)}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/>
</div>
)
const {func, shape, string} = PropTypes
const {bool, func, shape, string} = PropTypes
SchemaExplorer.defaultProps = {
isQuerySupportedByExplorer: true,
}
SchemaExplorer.propTypes = {
query: shape({
@ -80,6 +87,7 @@ SchemaExplorer.propTypes = {
}).isRequired,
source: shape({}),
initialGroupByTime: string.isRequired,
isQuerySupportedByExplorer: bool,
}
export default SchemaExplorer

View File

@ -40,6 +40,7 @@ interface Props {
querySource: Source
onChooseTag: () => void
onGroupByTag: () => void
isQuerySupportedByExplorer: boolean
}
interface State {
@ -129,7 +130,12 @@ class TagList extends PureComponent<Props, State> {
}
public render() {
const {query, onChooseTag, onGroupByTag} = this.props
const {
query,
onChooseTag,
onGroupByTag,
isQuerySupportedByExplorer,
} = this.props
return (
<div className="query-builder--sub-list">
@ -142,6 +148,7 @@ class TagList extends PureComponent<Props, State> {
onGroupByTag={onGroupByTag}
selectedTagValues={query.tags[tagKey] || []}
isUsingGroupBy={query.groupBy.tags.indexOf(tagKey) > -1}
isQuerySupportedByExplorer={isQuerySupportedByExplorer}
/>
))}
</div>

View File

@ -14,6 +14,7 @@ interface Props {
selectedTagValues: string[]
isUsingGroupBy?: boolean
onChooseTag: (tag: Tag) => void
isQuerySupportedByExplorer: boolean
onGroupByTag: (tagKey: string) => void
}
@ -36,10 +37,16 @@ class TagListItem extends PureComponent<Props, State> {
this.handleGroupBy = this.handleGroupBy.bind(this)
this.handleClickKey = this.handleClickKey.bind(this)
this.handleFilterText = this.handleFilterText.bind(this)
this.handleInputClick = this.handleInputClick.bind(this)
}
public handleChoose(tagValue: string, e: MouseEvent<HTMLElement>) {
e.stopPropagation()
const {isQuerySupportedByExplorer} = this.props
if (!isQuerySupportedByExplorer) {
return
}
this.props.onChooseTag({key: this.props.tagKey, value: tagValue})
}
@ -67,7 +74,11 @@ class TagListItem extends PureComponent<Props, State> {
}
public handleGroupBy(e) {
const {isQuerySupportedByExplorer} = this.props
e.stopPropagation()
if (!isQuerySupportedByExplorer) {
return
}
this.props.onGroupByTag(this.props.tagKey)
}

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types'
import Dropdown from 'shared/components/Dropdown'
import {TIME_SHIFTS} from 'shared/constants/timeShift'
const TimeShiftDropdown = ({selected, onChooseTimeShift}) => (
const TimeShiftDropdown = ({selected, onChooseTimeShift, isDisabled}) => (
<div className="group-by-time">
<label className="group-by-time--label">Compare:</label>
<Dropdown
@ -12,15 +12,17 @@ const TimeShiftDropdown = ({selected, onChooseTimeShift}) => (
items={TIME_SHIFTS}
onChoose={onChooseTimeShift}
selected={selected || 'none'}
disabled={isDisabled}
/>
</div>
)
const {func, string} = PropTypes
const {bool, func, string} = PropTypes
TimeShiftDropdown.propTypes = {
selected: string,
onChooseTimeShift: func.isRequired,
isDisabled: bool,
}
export default TimeShiftDropdown

View File

@ -410,6 +410,13 @@ export const VIS_VIEWS = [GRAPH, TABLE]
// InfluxQL Macros
export const TEMP_VAR_INTERVAL = ':interval:'
export const TEMP_VAR_DASHBOARD_TIME = ':dashboardTime:'
export const TEMP_VAR_UPPER_DASHBOARD_TIME = ':upperDashboardTime:'
export const PREDEFINED_TEMP_VARS = [
TEMP_VAR_INTERVAL,
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
]
export const INITIAL_GROUP_BY_TIME = '10s'
export const AUTO_GROUP_BY = 'auto'
@ -443,7 +450,7 @@ export const intervalValuesPoints = [
export const interval = {
id: 'interval',
type: 'autoGroupBy',
tempVar: ':interval:',
tempVar: TEMP_VAR_INTERVAL,
label: 'automatically determine the best group by time',
values: intervalValuesPoints,
}

View File

@ -8,6 +8,10 @@ import LayoutRenderer from 'shared/components/LayoutRenderer'
import {fixtureStatusPageCells} from 'src/status/fixtures'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
@ErrorHandling
class StatusPage extends Component {
@ -25,7 +29,7 @@ class StatusPage extends Component {
const dashboardTime = {
id: 'dashtime',
tempVar: ':dashboardTime:',
tempVar: TEMP_VAR_DASHBOARD_TIME,
type: 'constant',
values: [
{
@ -38,7 +42,7 @@ class StatusPage extends Component {
const upperDashboardTime = {
id: 'upperdashtime',
tempVar: ':upperDashboardTime:',
tempVar: TEMP_VAR_UPPER_DASHBOARD_TIME,
type: 'constant',
values: [
{

View File

@ -1,4 +1,5 @@
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
export const fixtureStatusPageCells = [
{
@ -13,8 +14,7 @@ export const fixtureStatusPageCells = [
colors: DEFAULT_LINE_COLORS,
queries: [
{
query:
'SELECT count("value") AS "count_value" FROM "chronograf"."autogen"."alerts" WHERE time > :dashboardTime: GROUP BY time(1d)',
query: `SELECT count("value") AS "count_value" FROM "chronograf"."autogen"."alerts" WHERE time > ${TEMP_VAR_DASHBOARD_TIME} GROUP BY time(1d)`,
label: 'Events',
queryConfig: {
database: 'chronograf',

View File

@ -1,5 +1,9 @@
import {buildQuery} from 'utils/influxql'
import {TYPE_SHIFTED, TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {
TEMP_VAR_DASHBOARD_TIME,
TEMP_VAR_UPPER_DASHBOARD_TIME,
} from 'src/shared/constants'
import {timeRanges} from 'shared/data/timeRanges'
const buildCannedDashboardQuery = (query, {lower, upper}, host) => {
@ -48,8 +52,8 @@ export const buildQueriesForLayouts = (cell, source, timeRange, host) => {
queryConfig: {database, measurement, fields, shifts, rawText, range},
} = query
const tR = range || {
upper: ':upperDashboardTime:',
lower: ':dashboardTime:',
upper: TEMP_VAR_UPPER_DASHBOARD_TIME,
lower: TEMP_VAR_DASHBOARD_TIME,
}
queryText =

View File

@ -13,6 +13,7 @@ const setup = (override = {}) => {
onToggleTagAcceptance: () => {},
query,
querySource: source,
isQuerySupportedByExplorer: true,
...override,
}

View File

@ -23,6 +23,7 @@ const setup = (overrides = {}) => {
measurement: 'test',
numTagsActive: 3,
areTagsAccepted: true,
isQuerySupportedByExplorer: true,
onChooseTag: () => {},
onGroupByTag: () => {},
onAcceptReject: () => {},

View File

@ -13,6 +13,7 @@ const setup = (override = {}) => {
onGroupByTag: () => {},
query,
querySource: source,
isQuerySupportedByExplorer: true,
...override,
}