diff --git a/CHANGELOG.md b/CHANGELOG.md index 919a36c51e7..6047d889bfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ 1. [12678](https://github.com/influxdata/influxdb/pull/12678): Enable the use of variables in the Data Explorer and Cell Editor Overlay 1. [12655](https://github.com/influxdata/influxdb/pull/12655): Add a variable control bar to dashboards to select values for variables. 1. [12706](https://github.com/influxdata/influxdb/pull/12706): Add ability to add variable to script from the side menu. +1. [12791](https://github.com/influxdata/influxdb/pull/12791): Use time range for metaqueries in Data Explorer and Cell Editor Overlay ### Bug Fixes diff --git a/ui/src/timeMachine/actions/index.ts b/ui/src/timeMachine/actions/index.ts index 0bcaad6ef43..ba0d10f702f 100644 --- a/ui/src/timeMachine/actions/index.ts +++ b/ui/src/timeMachine/actions/index.ts @@ -5,7 +5,10 @@ import {saveAndExecuteQueries} from 'src/timeMachine/actions/queries' // Types import {Dispatch} from 'redux-thunk' import {TimeMachineState} from 'src/timeMachine/reducers' -import {Action as QueryBuilderAction} from 'src/timeMachine/actions/queryBuilder' +import { + reloadTagSelectors, + Action as QueryBuilderAction, +} from 'src/timeMachine/actions/queryBuilder' import {Action as QueryResultsAction} from 'src/timeMachine/actions/queries' import {TimeRange, ViewType} from 'src/types/v2' import { @@ -114,6 +117,7 @@ const setTimeRangeSync = (timeRange: TimeRange): SetTimeRangeAction => ({ export const setTimeRange = (timeRange: TimeRange) => dispatch => { dispatch(setTimeRangeSync(timeRange)) dispatch(saveAndExecuteQueries()) + dispatch(reloadTagSelectors()) } interface SetTypeAction { diff --git a/ui/src/timeMachine/actions/queryBuilder.ts b/ui/src/timeMachine/actions/queryBuilder.ts index f02612d2dd2..fde656eafcd 100644 --- a/ui/src/timeMachine/actions/queryBuilder.ts +++ b/ui/src/timeMachine/actions/queryBuilder.ts @@ -26,6 +26,7 @@ export type Action = | SelectFunctionAction | SetValuesSearchTermAction | SetKeysSearchTermAction + | SetBuilderTagsStatusAction interface SetBuilderBucketsStatusAction { type: 'SET_BUILDER_BUCKETS_STATUS' @@ -64,6 +65,18 @@ const setBuilderBucket = ( payload: {bucket, resetSelections}, }) +interface SetBuilderTagsStatusAction { + type: 'SET_BUILDER_TAGS_STATUS' + payload: {status: RemoteDataState} +} + +export const setBuilderTagsStatus = ( + status: RemoteDataState +): SetBuilderTagsStatusAction => ({ + type: 'SET_BUILDER_TAGS_STATUS', + payload: {status}, +}) + interface SetBuilderTagKeysAction { type: 'SET_BUILDER_TAG_KEYS' payload: {index: number; keys: string[]} @@ -206,7 +219,11 @@ export const loadBuckets = () => async ( dispatch(setBuilderBucketsStatus(RemoteDataState.Loading)) try { - const buckets = await queryBuilderFetcher.findBuckets(queryURL, orgID) + const buckets = await queryBuilderFetcher.findBuckets({ + url: queryURL, + orgID, + }) + const selectedBucket = getActiveQuery(getState()).builderConfig.buckets[0] dispatch(setBuilderBuckets(buckets)) @@ -244,24 +261,25 @@ export const loadTagSelector = (index: number) => async ( return } - const tagPredicates = tags.slice(0, index) + const tagsSelections = tags.slice(0, index) const queryURL = getState().links.query.self const orgID = getActiveOrg(getState()).id dispatch(setBuilderTagKeysStatus(index, RemoteDataState.Loading)) try { + const timeRange = getActiveTimeMachine(getState()).timeRange const searchTerm = getActiveTimeMachine(getState()).queryBuilder.tags[index] .keysSearchTerm - const keys = await queryBuilderFetcher.findKeys( - index, - queryURL, + const keys = await queryBuilderFetcher.findKeys(index, { + url: queryURL, orgID, - buckets[0], - tagPredicates, - searchTerm - ) + bucket: buckets[0], + tagsSelections, + searchTerm, + timeRange, + }) const {key} = tags[index] @@ -299,25 +317,27 @@ const loadTagSelectorValues = (index: number) => async ( ) => { const state = getState() const {buckets, tags} = getActiveQuery(state).builderConfig - const tagPredicates = tags.slice(0, index) + const tagsSelections = tags.slice(0, index) const queryURL = state.links.query.self const orgID = getActiveOrg(state).id dispatch(setBuilderTagValuesStatus(index, RemoteDataState.Loading)) try { + const timeRange = getActiveTimeMachine(getState()).timeRange const key = getActiveQuery(getState()).builderConfig.tags[index].key const searchTerm = getActiveTimeMachine(getState()).queryBuilder.tags[index] .valuesSearchTerm - const values = await queryBuilderFetcher.findValues( - index, - queryURL, + + const values = await queryBuilderFetcher.findValues(index, { + url: queryURL, orgID, - buckets[0], - tagPredicates, + bucket: buckets[0], + tagsSelections, key, - searchTerm - ) + searchTerm, + timeRange, + }) const {values: selectedValues} = tags[index] @@ -404,3 +424,8 @@ export const removeTagSelector = (index: number) => async ( dispatch(removeTagSelectorSync(index)) dispatch(loadTagSelector(index)) } + +export const reloadTagSelectors = () => async (dispatch: Dispatch) => { + dispatch(setBuilderTagsStatus(RemoteDataState.Loading)) + dispatch(loadTagSelector(0)) +} diff --git a/ui/src/timeMachine/apis/QueryBuilderFetcher.ts b/ui/src/timeMachine/apis/QueryBuilderFetcher.ts index 3f5c2495555..f35d803089d 100644 --- a/ui/src/timeMachine/apis/QueryBuilderFetcher.ts +++ b/ui/src/timeMachine/apis/QueryBuilderFetcher.ts @@ -3,10 +3,12 @@ import { findBuckets, findKeys, findValues, + FindBucketsOptions, + FindKeysOptions, + FindValuesOptions, } from 'src/timeMachine/apis/queryBuilder' // Types -import {BuilderConfig} from 'src/types/v2' import {WrappedCancelablePromise} from 'src/types/promises' type CancelableQuery = WrappedCancelablePromise @@ -19,17 +21,17 @@ class QueryBuilderFetcher { private findValuesCache: {[key: string]: string[]} = {} private findBucketsCache: {[key: string]: string[]} = {} - public async findBuckets(url: string, orgID: string): Promise { + public async findBuckets(options: FindBucketsOptions): Promise { this.cancelFindBuckets() - const cacheKey = JSON.stringify([...arguments]) + const cacheKey = JSON.stringify(options) const cachedResult = this.findBucketsCache[cacheKey] if (cachedResult) { return Promise.resolve(cachedResult) } - const pendingResult = findBuckets(url, orgID) + const pendingResult = findBuckets(options) pendingResult.promise.then(result => { this.findBucketsCache[cacheKey] = result @@ -46,28 +48,18 @@ class QueryBuilderFetcher { public async findKeys( index: number, - url: string, - orgID: string, - bucket: string, - tagsSelections: BuilderConfig['tags'], - searchTerm: string = '' + options: FindKeysOptions ): Promise { this.cancelFindKeys(index) - const cacheKey = JSON.stringify([...arguments].slice(1)) + const cacheKey = JSON.stringify(options) const cachedResult = this.findKeysCache[cacheKey] if (cachedResult) { return Promise.resolve(cachedResult) } - const pendingResult = findKeys( - url, - orgID, - bucket, - tagsSelections, - searchTerm - ) + const pendingResult = findKeys(options) this.findKeysQueries[index] = pendingResult @@ -86,30 +78,18 @@ class QueryBuilderFetcher { public async findValues( index: number, - url: string, - orgID: string, - bucket: string, - tagsSelections: BuilderConfig['tags'], - key: string, - searchTerm: string = '' + options: FindValuesOptions ): Promise { this.cancelFindValues(index) - const cacheKey = JSON.stringify([...arguments].slice(1)) + const cacheKey = JSON.stringify(options) const cachedResult = this.findValuesCache[cacheKey] if (cachedResult) { return Promise.resolve(cachedResult) } - const pendingResult = findValues( - url, - orgID, - bucket, - tagsSelections, - key, - searchTerm - ) + const pendingResult = findValues(options) this.findValuesQueries[index] = pendingResult diff --git a/ui/src/timeMachine/apis/queryBuilder.ts b/ui/src/timeMachine/apis/queryBuilder.ts index f97c3f82ef6..7e8bdf23fde 100644 --- a/ui/src/timeMachine/apis/queryBuilder.ts +++ b/ui/src/timeMachine/apis/queryBuilder.ts @@ -5,19 +5,29 @@ import {get} from 'lodash' import {executeQuery, ExecuteFluxQueryResult} from 'src/shared/apis/query' import {parseResponse} from 'src/shared/parsing/flux/response' +// Utils +import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars' +import {formatExpression} from 'src/variables/utils/formatExpression' + // Types import {BuilderConfig} from 'src/types/v2' +import {TimeRange} from 'src/types' import {WrappedCancelablePromise} from 'src/types/promises' -export const SEARCH_DURATION = '30d' -export const LIMIT = 200 +const DEFAULT_TIME_RANGE: TimeRange = {lower: 'now() - 30d'} +const DEFAULT_LIMIT = 200 type CancelableQuery = WrappedCancelablePromise -export function findBuckets(url: string, orgID: string): CancelableQuery { +export interface FindBucketsOptions { + url: string + orgID: string +} + +export function findBuckets({url, orgID}: FindBucketsOptions): CancelableQuery { const query = `buckets() |> sort(columns: ["name"]) - |> limit(n: ${LIMIT})` + |> limit(n: ${DEFAULT_LIMIT})` const {promise, cancel} = executeQuery(url, orgID, query) @@ -27,28 +37,41 @@ export function findBuckets(url: string, orgID: string): CancelableQuery { } } -export function findKeys( - url: string, - orgID: string, - bucket: string, - tagsSelections: BuilderConfig['tags'], - searchTerm: string = '' -): CancelableQuery { +export interface FindKeysOptions { + url: string + orgID: string + bucket: string + tagsSelections: BuilderConfig['tags'] + searchTerm?: string + timeRange?: TimeRange + limit?: number +} + +export function findKeys({ + url, + orgID, + bucket, + tagsSelections, + searchTerm = '', + timeRange = DEFAULT_TIME_RANGE, + limit = DEFAULT_LIMIT, +}: FindKeysOptions): CancelableQuery { const tagFilters = formatTagFilterPredicate(tagsSelections) const searchFilter = formatSearchFilterCall(searchTerm) const previousKeyFilter = formatTagKeyFilterCall(tagsSelections) - - const query = `import "influxdata/influxdb/v1" - -v1.tagKeys(bucket: "${bucket}", predicate: ${tagFilters}, start: -${SEARCH_DURATION})${searchFilter}${previousKeyFilter} - |> filter(fn: (r) => - r._value != "_time" and - r._value != "_start" and - r._value != "_stop" and - r._value != "_value") + const timeRangeArguments = formatTimeRangeArguments(timeRange) + + // TODO: Use the `v1.tagKeys` function from the Flux standard library once + // this issue is resolved: https://github.com/influxdata/flux/issues/1071 + const query = `from(bucket: "${bucket}") + |> range(${timeRangeArguments}) + |> filter(fn: ${tagFilters}) + |> keys() + |> keep(columns: ["_value"])${searchFilter}${previousKeyFilter} + |> filter(fn: (r) => r._value != "_time" and r._value != "_start" and r._value != "_stop" and r._value != "_value") |> distinct() |> sort() - |> limit(n: ${LIMIT})` + |> limit(n: ${limit})` const {promise, cancel} = executeQuery(url, orgID, query) @@ -58,21 +81,40 @@ v1.tagKeys(bucket: "${bucket}", predicate: ${tagFilters}, start: -${SEARCH_DURAT } } -export function findValues( - url: string, - orgID: string, - bucket: string, - tagsSelections: BuilderConfig['tags'], - key: string, - searchTerm: string = '' -): CancelableQuery { +export interface FindValuesOptions { + url: string + orgID: string + bucket: string + tagsSelections: BuilderConfig['tags'] + key: string + searchTerm?: string + timeRange?: TimeRange + limit?: number +} + +export function findValues({ + url, + orgID, + bucket, + tagsSelections, + key, + searchTerm = '', + timeRange = DEFAULT_TIME_RANGE, + limit = DEFAULT_LIMIT, +}: FindValuesOptions): CancelableQuery { const tagFilters = formatTagFilterPredicate(tagsSelections) const searchFilter = formatSearchFilterCall(searchTerm) - - const query = `import "influxdata/influxdb/v1" - -v1.tagValues(bucket: "${bucket}", tag: "${key}", predicate: ${tagFilters}, start: -${SEARCH_DURATION})${searchFilter} - |> limit(n: ${LIMIT}) + const timeRangeArguments = formatTimeRangeArguments(timeRange) + + // TODO: Use the `v1.tagValues` function from the Flux standard library once + // this issue is resolved: https://github.com/influxdata/flux/issues/1071 + const query = `from(bucket: "${bucket}") + |> range(${timeRangeArguments}) + |> filter(fn: ${tagFilters}) + |> group(columns: ["${key}"]) + |> distinct(column: "${key}") + |> keep(columns: ["_value"])${searchFilter} + |> limit(n: ${limit}) |> sort()` const {promise, cancel} = executeQuery(url, orgID, query) @@ -150,3 +192,11 @@ export function formatSearchFilterCall(searchTerm: string) { return `\n |> filter(fn: (r) => r._value =~ /(?i:${searchTerm})/)` } + +export function formatTimeRangeArguments(timeRange: TimeRange): string { + const [start, stop] = getTimeRangeVars(timeRange).map(assignment => + formatExpression(assignment.init) + ) + + return `start: ${start}, stop: ${stop}` +} diff --git a/ui/src/timeMachine/components/TagSelector.scss b/ui/src/timeMachine/components/TagSelector.scss index 551eda34400..828a8423804 100644 --- a/ui/src/timeMachine/components/TagSelector.scss +++ b/ui/src/timeMachine/components/TagSelector.scss @@ -52,12 +52,20 @@ button.tag-selector--remove, pointer-events: none; flex: 1 1 0; display: flex; + flex-direction: column; justify-content: center; align-items: center; + padding: $ix-marg-c $ix-marg-c $ix-marg-d $ix-marg-c; color: $g8-storm; font-weight: 500; font-size: 16px; - text-transform: uppercase; text-align: center; - padding: 12px 12px 36px 12px; + text-transform: uppercase; + + small { + display: block; + font-size: 14px; + text-transform: none; + margin-top: $ix-marg-b; + } } diff --git a/ui/src/timeMachine/components/TagSelector.tsx b/ui/src/timeMachine/components/TagSelector.tsx index 6ac841eef5e..fe622809bc0 100644 --- a/ui/src/timeMachine/components/TagSelector.tsx +++ b/ui/src/timeMachine/components/TagSelector.tsx @@ -102,7 +102,7 @@ class TagSelector extends PureComponent { <>
{this.removeButton}
- No more tag keys found + No tag keys found in the current time range
) @@ -181,7 +181,11 @@ class TagSelector extends PureComponent { } if (valuesStatus === RemoteDataState.Done && !values.length) { - return
Nothing found
+ return ( +
+ No values found in the current time range +
+ ) } return ( diff --git a/ui/src/timeMachine/reducers/index.ts b/ui/src/timeMachine/reducers/index.ts index 0d6b8e4d1b0..4fd8ca9b784 100644 --- a/ui/src/timeMachine/reducers/index.ts +++ b/ui/src/timeMachine/reducers/index.ts @@ -556,6 +556,18 @@ export const timeMachineReducer = ( }) } + case 'SET_BUILDER_TAGS_STATUS': { + return produce(state, draftState => { + const {status} = action.payload + const tags = draftState.queryBuilder.tags + + for (const tag of tags) { + tag.keysStatus = status + tag.valuesStatus = status + } + }) + } + case 'SET_BUILDER_TAG_KEYS': { return produce(state, draftState => { const {index, keys} = action.payload diff --git a/ui/src/variables/utils/formatExpression.ts b/ui/src/variables/utils/formatExpression.ts new file mode 100644 index 00000000000..c4033f24893 --- /dev/null +++ b/ui/src/variables/utils/formatExpression.ts @@ -0,0 +1,36 @@ +import {Expression} from 'src/types/ast' + +export const formatExpression = (expr: Expression): string => { + switch (expr.type) { + case 'DateTimeLiteral': + case 'BooleanLiteral': + case 'UnsignedIntegerLiteral': + case 'IntegerLiteral': + return String(expr.value) + case 'StringLiteral': + return `"${expr.value}"` + case 'DurationLiteral': + return expr.values.reduce( + (acc, {magnitude, unit}) => `${acc}${magnitude}${unit}`, + '' + ) + case 'FloatLiteral': + return String(expr.value).includes('.') + ? String(expr.value) + : expr.value.toFixed(1) + case 'UnaryExpression': + return `${expr.operator}${formatExpression(expr.argument)}` + case 'BinaryExpression': + return `${formatExpression(expr.left)} ${ + expr.operator + } ${formatExpression(expr.right)}` + case 'CallExpression': + // This doesn't handle formatting a call expression with arguments, or + // with any other sort of callee except an `Identifier` + return `${formatExpression(expr.callee)}()` + case 'Identifier': + return expr.name + default: + throw new Error(`cant format expression of type ${expr.type}`) + } +} diff --git a/ui/src/variables/utils/formatVarsOption.ts b/ui/src/variables/utils/formatVarsOption.ts index d66bc9eb61f..67016897c00 100644 --- a/ui/src/variables/utils/formatVarsOption.ts +++ b/ui/src/variables/utils/formatVarsOption.ts @@ -1,5 +1,6 @@ import {OPTION_NAME} from 'src/variables/constants/index' -import {VariableAssignment, Expression} from 'src/types/ast' +import {formatExpression} from 'src/variables/utils/formatExpression' +import {VariableAssignment} from 'src/types/ast' export const formatVarsOption = (variables: VariableAssignment[]): string => { if (!variables.length) { @@ -14,38 +15,3 @@ export const formatVarsOption = (variables: VariableAssignment[]): string => { return option } - -const formatExpression = (expr: Expression): string => { - switch (expr.type) { - case 'DateTimeLiteral': - case 'BooleanLiteral': - case 'UnsignedIntegerLiteral': - case 'IntegerLiteral': - return String(expr.value) - case 'StringLiteral': - return `"${expr.value}"` - case 'DurationLiteral': - return expr.values.reduce( - (acc, {magnitude, unit}) => `${acc}${magnitude}${unit}`, - '' - ) - case 'FloatLiteral': - return String(expr.value).includes('.') - ? String(expr.value) - : expr.value.toFixed(1) - case 'UnaryExpression': - return `${expr.operator}${formatExpression(expr.argument)}` - case 'BinaryExpression': - return `${formatExpression(expr.left)} ${ - expr.operator - } ${formatExpression(expr.right)}` - case 'CallExpression': - // This doesn't handle formatting a call expression with arguments, or - // with any other sort of callee except an `Identifier` - return `${formatExpression(expr.callee)}()` - case 'Identifier': - return expr.name - default: - throw new Error(`cant format expression of type ${expr.type}`) - } -}