diff --git a/ui/src/timeMachine/actions/queryBuilder.ts b/ui/src/timeMachine/actions/queryBuilder.ts index 40b7e46734a..9e9f09d5987 100644 --- a/ui/src/timeMachine/actions/queryBuilder.ts +++ b/ui/src/timeMachine/actions/queryBuilder.ts @@ -285,7 +285,8 @@ export const selectTagValue = (index: number, value: string) => ( timeMachines: {activeTimeMachineID}, } = state const tags = getActiveQuery(state).builderConfig.tags - const values = tags[index].values + const currentTag = tags[index] + const values = currentTag.values let newValues: string[] @@ -293,7 +294,7 @@ export const selectTagValue = (index: number, value: string) => ( newValues = values.filter(v => v !== value) } else if ( activeTimeMachineID === 'alerting' && - tags[index].key === '_field' + currentTag.key === '_field' ) { newValues = [value] } else { @@ -302,6 +303,11 @@ export const selectTagValue = (index: number, value: string) => ( dispatch(setBuilderTagValuesSelection(index, newValues)) + // don't add a new tag filter if we're grouping + if (currentTag.aggregateFunctionType === 'group') { + return + } + if (index === tags.length - 1 && newValues.length) { dispatch(addTagSelector()) } else { diff --git a/ui/src/timeMachine/components/TagSelector.tsx b/ui/src/timeMachine/components/TagSelector.tsx index 4ee03f1b476..3cba6d27531 100644 --- a/ui/src/timeMachine/components/TagSelector.tsx +++ b/ui/src/timeMachine/components/TagSelector.tsx @@ -80,8 +80,6 @@ type Props = StateProps & DispatchProps & OwnProps class TagSelector extends PureComponent { private debouncer = new DefaultDebouncer() - // bucky: this will currently always be 'Filter' - // updates to this are imminent private renderAggregateFunctionType( aggregateFunctionType: BuilderAggregateFunctionType ) { @@ -96,9 +94,13 @@ class TagSelector extends PureComponent { return ( - {this.body} @@ -265,17 +267,25 @@ class TagSelector extends PureComponent { onSearchValues(index) } + + private handleAggregateFunctionSelect = ( + option: BuilderAggregateFunctionType + ) => { + const {index, onSetBuilderAggregateFunctionType} = this.props + onSetBuilderAggregateFunctionType(option, index) + } } const mstp = (state: AppState, ownProps: OwnProps): StateProps => { + const activeQueryBuilder = getActiveTimeMachine(state).queryBuilder + const { keys, keysSearchTerm, keysStatus, - values, valuesSearchTerm, valuesStatus, - } = getActiveTimeMachine(state).queryBuilder.tags[ownProps.index] + } = activeQueryBuilder.tags[ownProps.index] const tags = getActiveQuery(state).builderConfig.tags const { @@ -285,13 +295,32 @@ const mstp = (state: AppState, ownProps: OwnProps): StateProps => { } = tags[ownProps.index] let emptyText: string - - if (ownProps.index === 0 || !tags[ownProps.index - 1].key) { + const previousTagSelector = tags[ownProps.index - 1] + if ( + ownProps.index === 0 || + !previousTagSelector || + !previousTagSelector.key + ) { emptyText = '' } else { emptyText = `Select a ${tags[ownProps.index - 1].key} value first` } + // if we're grouping, we want to be able to group on all previous tags + let {values} = activeQueryBuilder.tags[ownProps.index] + if (aggregateFunctionType === 'group') { + values = [] + activeQueryBuilder.tags.forEach((tag, i) => { + // if we don't skip the current set of tags, we'll double render them at the bottom of the selector list + if (i === ownProps.index) { + return + } + tag.values.forEach(value => { + values.push(value) + }) + }) + } + const isInCheckOverlay = getIsInCheckOverlay(state) return { diff --git a/ui/src/timeMachine/components/builderCard/BuilderCard.tsx b/ui/src/timeMachine/components/builderCard/BuilderCard.tsx index a60bc245593..39f8aa1b72d 100644 --- a/ui/src/timeMachine/components/builderCard/BuilderCard.tsx +++ b/ui/src/timeMachine/components/builderCard/BuilderCard.tsx @@ -4,6 +4,7 @@ import classnames from 'classnames' // Components import BuilderCardHeader from 'src/timeMachine/components/builderCard/BuilderCardHeader' +import BuilderCardDropdownHeader from 'src/timeMachine/components/builderCard/BuilderCardDropdownHeader' import BuilderCardMenu from 'src/timeMachine/components/builderCard/BuilderCardMenu' import BuilderCardBody from 'src/timeMachine/components/builderCard/BuilderCardBody' import BuilderCardEmpty from 'src/timeMachine/components/builderCard/BuilderCardEmpty' @@ -16,6 +17,7 @@ interface Props { export default class BuilderCard extends PureComponent { public static Header = BuilderCardHeader + public static DropdownHeader = BuilderCardDropdownHeader public static Menu = BuilderCardMenu public static Body = BuilderCardBody public static Empty = BuilderCardEmpty diff --git a/ui/src/timeMachine/components/builderCard/BuilderCardDropdownHeader.tsx b/ui/src/timeMachine/components/builderCard/BuilderCardDropdownHeader.tsx new file mode 100644 index 00000000000..3a681e62234 --- /dev/null +++ b/ui/src/timeMachine/components/builderCard/BuilderCardDropdownHeader.tsx @@ -0,0 +1,47 @@ +// Libraries +import React, {PureComponent} from 'react' + +import {SelectDropdown} from '@influxdata/clockface' +import {BuilderAggregateFunctionType} from 'src/client' + +interface Props { + options: string[] + selectedOption: string + testID: string + onSelect?: (option: BuilderAggregateFunctionType) => void + onDelete?: () => void +} + +const emptyFunction = () => {} + +export default class BuilderCardDropdownHeader extends PureComponent { + public static defaultProps = { + testID: 'builder-card--header', + } + + public render() { + const {children, options, onSelect, selectedOption, testID} = this.props + + return ( +
+ + + {children} + {this.deleteButton} +
+ ) + } + + private get deleteButton(): JSX.Element | undefined { + const {onDelete} = this.props + + if (onDelete) { + return
+ } + } +} diff --git a/ui/src/timeMachine/reducers/index.ts b/ui/src/timeMachine/reducers/index.ts index 60830b27c31..a11f1ec60b5 100644 --- a/ui/src/timeMachine/reducers/index.ts +++ b/ui/src/timeMachine/reducers/index.ts @@ -671,11 +671,17 @@ export const timeMachineReducer = ( const {index, builderAggregateFunctionType} = action.payload const draftQuery = draftState.draftQueries[draftState.activeQueryIndex] + buildActiveQuery(draftState) if ( draftQuery && draftQuery.builderConfig && draftQuery.builderConfig.tags[index] ) { + + // When switching between filtering and grouping + // we want to clear out any previously selected values + draftQuery.builderConfig.tags[index].values = [] + draftQuery.builderConfig.tags[ index ].aggregateFunctionType = builderAggregateFunctionType @@ -761,6 +767,8 @@ export const timeMachineReducer = ( draftState.queryBuilder.tags[index].values = values draftState.queryBuilder.tags[index].valuesStatus = RemoteDataState.Done + + buildActiveQuery(draftState) }) } @@ -829,10 +837,13 @@ export const timeMachineReducer = ( const {index} = action.payload const draftQuery = draftState.draftQueries[draftState.activeQueryIndex] - const selectedValues = draftQuery.builderConfig.tags[index].values + let selectedValues = [] + if (draftQuery) { + selectedValues = draftQuery.builderConfig.tags[index].values - draftQuery.builderConfig.tags.splice(index, 1) - draftState.queryBuilder.tags.splice(index, 1) + draftQuery.builderConfig.tags.splice(index, 1) + draftState.queryBuilder.tags.splice(index, 1) + } if (selectedValues.length) { buildActiveQuery(draftState) diff --git a/ui/src/timeMachine/utils/queryBuilder.ts b/ui/src/timeMachine/utils/queryBuilder.ts index e9569bd3123..1c1a21ee79e 100644 --- a/ui/src/timeMachine/utils/queryBuilder.ts +++ b/ui/src/timeMachine/utils/queryBuilder.ts @@ -8,7 +8,7 @@ import { WINDOW_PERIOD, } from 'src/variables/constants' import {AGG_WINDOW_AUTO} from 'src/timeMachine/constants/queryBuilder' -import {BuilderTagsType} from '@influxdata/influx' +import {BuilderTagsType} from 'src/client/' export function isConfigValid(builderConfig: BuilderConfig): boolean { const {buckets, tags} = builderConfig @@ -81,15 +81,19 @@ export const isCheckSaveable = ( ) } -export function buildQuery(builderConfig: BuilderConfig): string { +export function buildQuery( + builderConfig: BuilderConfig, +): string { const {functions} = builderConfig let query: string if (functions.length) { - query = functions.map(f => buildQueryHelper(builderConfig, f)).join('\n\n') + query = functions + .map(f => buildQueryHelper(builderConfig, f)) + .join('\n\n') } else { - query = buildQueryHelper(builderConfig) + query = buildQueryHelper(builderConfig, null) } return query @@ -97,15 +101,24 @@ export function buildQuery(builderConfig: BuilderConfig): string { function buildQueryHelper( builderConfig: BuilderConfig, - fn?: BuilderConfig['functions'][0] + fn?: BuilderConfig['functions'][0], ): string { const [bucket] = builderConfig.buckets - const tagFilterCall = formatTagFilterCall(builderConfig.tags) + + const tags = Array.from(builderConfig.tags) + + // todo: (bucky) - check to see if we can combine filter calls + // https://github.com/influxdata/influxdb/issues/16076 + let tagsFunctionCalls = '' + tags.forEach(tag => { + tagsFunctionCalls += convertTagsToFluxFunctionString(tag) + }) + const {aggregateWindow} = builderConfig const fnCall = fn ? formatFunctionCall(fn, aggregateWindow.period) : '' const query = `from(bucket: "${bucket}") - |> range(start: ${OPTION_NAME}.${TIME_RANGE_START}, stop: ${OPTION_NAME}.${TIME_RANGE_STOP})${tagFilterCall}${fnCall}` + |> range(start: ${OPTION_NAME}.${TIME_RANGE_START}, stop: ${OPTION_NAME}.${TIME_RANGE_STOP})${tagsFunctionCalls}${fnCall}` return query } @@ -125,29 +138,39 @@ export function formatFunctionCall( return `\n ${fnSpec.flux(formattedPeriod)}\n |> yield(name: "${fn.name}")` } -const formatPeriod = (period: string): string => { - if (period === AGG_WINDOW_AUTO || !period) { - return `${OPTION_NAME}.${WINDOW_PERIOD}` +const convertTagsToFluxFunctionString = function convertTagsToFluxFunctionString(tag: BuilderTagsType) { + if (!tag.key) { + return '' } - return period -} + if (tag.aggregateFunctionType === 'filter') { + if (!tag.values.length) { + return '' + } -function formatTagFilterCall(tagsSelections: BuilderConfig['tags']) { - if (!tagsSelections.length) { - return '' + const fnBody = tag.values.map(value => `r.${tag.key} == "${value}"`).join(' or ') + return `\n |> filter(fn: (r) => ${fnBody})` } - const calls = tagsSelections - .filter(({key, values}) => key && values.length) - .map(({key, values}) => { - const fnBody = values.map(value => `r.${key} == "${value}"`).join(' or ') + if (tag.aggregateFunctionType === 'group') { + const quotedValues = tag.values.map(value => `"${value}"`) // wrap the value in double quotes + + if (quotedValues.length) { + return `\n |> group(columns: [${quotedValues.join(', ')}])` // join with a comma (e.g. "foo","bar","baz") + } + + return '\n |> group()' + } - return `|> filter(fn: (r) => ${fnBody})` - }) - .join('\n ') + return '' +} - return `\n ${calls}` +const formatPeriod = (period: string): string => { + if (period === AGG_WINDOW_AUTO || !period) { + return `${OPTION_NAME}.${WINDOW_PERIOD}` + } + + return period } export enum ConfirmationState {