feat: Add group() to Query Builder

pull/16337/head
Bucky Schwarz 2019-11-27 13:00:41 -08:00 committed by Bucky Schwarz
parent 5569193116
commit dd41af3d7a
13 changed files with 353 additions and 40 deletions

View File

@ -3,11 +3,11 @@
### Features
1. [16234](https://github.com/influxdata/influxdb/pull/16234): Add support for notification endpoints to influx templates/pkgs.
2. [16242](https://github.com/influxdata/influxdb/pull/16242): Drop id prefix for secret key requirement for notification endpoints
3. [16259](https://github.com/influxdata/influxdb/pull/16259): Add support for check resource to pkger parser
4. [16262](https://github.com/influxdata/influxdb/pull/16262): Add support for check resource pkger dry run functionality
5. [16275](https://github.com/influxdata/influxdb/pull/16275): Add support for check resource pkger apply functionality
6. [16283](https://github.com/influxdata/influxdb/pull/16283): Add support for check resource pkger export functionality
1. [16242](https://github.com/influxdata/influxdb/pull/16242): Drop id prefix for secret key requirement for notification endpoints
1. [16259](https://github.com/influxdata/influxdb/pull/16259): Add support for check resource to pkger parser
1. [16262](https://github.com/influxdata/influxdb/pull/16262): Add support for check resource pkger dry run functionality
1. [16275](https://github.com/influxdata/influxdb/pull/16275): Add support for check resource pkger apply functionality
1. [16283](https://github.com/influxdata/influxdb/pull/16283): Add support for check resource pkger export functionality
1. [16212](https://github.com/influxdata/influxdb/pull/16212): Add new kv.ForwardCursor interface
1. [16297](https://github.com/influxdata/influxdb/pull/16297): Add support for notification rule to pkger parser
1. [16298](https://github.com/influxdata/influxdb/pull/16298): Add support for notification rule pkger dry run functionality
@ -17,6 +17,7 @@
1. [16322](https://github.com/influxdata/influxdb/pull/16322): Add support for tasks to pkger dry run functionality
1. [16323](https://github.com/influxdata/influxdb/pull/16323): Add support for tasks to pkger apply functionality
1. [16324](https://github.com/influxdata/influxdb/pull/16324): Add support for tasks to pkger export functionality
1. [16226](https://github.com/influxdata/influxdb/pull/16226): Add group() to Query Builder
### Bug Fixes

View File

@ -349,7 +349,8 @@ describe('DataExplorer', () => {
})
})
describe('raw script editing', () => {
// todo: investigate flakiness of this test: https://github.com/influxdata/influxdb/issues/16330
describe.skip('raw script editing', () => {
beforeEach(() => {
cy.getByTestID('switch-to-script-editor').click()
})

View File

@ -103,7 +103,7 @@ logfile = ""
hostname = ""
## If set to true, do no set the "host" tag in the telegraf agent.
omit_hostname = false
`
`,
},
{
name: '__default__',

View File

@ -7,6 +7,7 @@ export const OSS_FLAGS = {
monacoEditor: false,
downloadCellCSV: false,
telegrafEditor: false,
queryBuilderGrouping: false,
}
export const CLOUD_FLAGS = {
@ -16,6 +17,7 @@ export const CLOUD_FLAGS = {
cloudBilling: CLOUD_BILLING_VISIBLE, // should be visible in dev and acceptance, but not in cloud
downloadCellCSV: false,
telegrafEditor: false,
queryBuilderGrouping: false,
}
export const isFlagEnabled = (flagName: string, equals?: string | boolean) => {

View File

@ -10,8 +10,11 @@ import {
// Types
import {Dispatch} from 'redux-thunk'
import {GetState, RemoteDataState} from 'src/types'
import {BuilderAggregateFunctionType} from 'src/client/generatedRoutes'
import {
BuilderAggregateFunctionType,
GetState,
RemoteDataState,
} from 'src/types'
import {BuilderFunctionsType} from '@influxdata/influx'
export type Action =
@ -285,7 +288,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 +297,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 +306,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 {

View File

@ -31,17 +31,22 @@ import {
} from 'src/timeMachine/actions/queryBuilder'
// Utils
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
import DefaultDebouncer from 'src/shared/utils/debouncer'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
import {
getActiveQuery,
getActiveTagValues,
getActiveTimeMachine,
getIsInCheckOverlay,
} from 'src/timeMachine/selectors'
// Types
import {AppState, RemoteDataState} from 'src/types'
import {BuilderAggregateFunctionType} from 'src/client'
import {
AppState,
BuilderAggregateFunctionType,
RemoteDataState,
} from 'src/types'
const SEARCH_DEBOUNCE_MS = 500
@ -80,8 +85,6 @@ type Props = StateProps & DispatchProps & OwnProps
class TagSelector extends PureComponent<Props> {
private debouncer = new DefaultDebouncer()
// bucky: this will currently always be 'Filter'
// updates to this are imminent
private renderAggregateFunctionType(
aggregateFunctionType: BuilderAggregateFunctionType
) {
@ -92,19 +95,32 @@ class TagSelector extends PureComponent<Props> {
}
public render() {
const {aggregateFunctionType, index} = this.props
return (
<BuilderCard>
<BuilderCard.Header
title={this.renderAggregateFunctionType(aggregateFunctionType)}
onDelete={index !== 0 && this.handleRemoveTagSelector}
/>
{this.header}
{this.body}
</BuilderCard>
)
}
private get header() {
const {aggregateFunctionType, index} = this.props
return isFlagEnabled('queryBuilderGrouping') ? (
<BuilderCard.DropdownHeader
options={['filter', 'group']}
selectedOption={this.renderAggregateFunctionType(aggregateFunctionType)}
onDelete={index !== 0 && this.handleRemoveTagSelector}
onSelect={this.handleAggregateFunctionSelect}
/>
) : (
<BuilderCard.Header
title={this.renderAggregateFunctionType(aggregateFunctionType)}
onDelete={index !== 0 && this.handleRemoveTagSelector}
/>
)
}
private get body() {
const {
index,
@ -265,33 +281,51 @@ class TagSelector extends PureComponent<Props> {
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
let emptyText: string
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`
}
const {
key: selectedKey,
values: selectedValues,
aggregateFunctionType,
} = tags[ownProps.index]
let emptyText: string
if (ownProps.index === 0 || !tags[ownProps.index - 1].key) {
emptyText = ''
} else {
emptyText = `Select a ${tags[ownProps.index - 1].key} value first`
}
const values = getActiveTagValues(
activeQueryBuilder.tags,
aggregateFunctionType,
ownProps.index
)
const isInCheckOverlay = getIsInCheckOverlay(state)
return {

View File

@ -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<Props> {
public static Header = BuilderCardHeader
public static DropdownHeader = BuilderCardDropdownHeader
public static Menu = BuilderCardMenu
public static Body = BuilderCardBody
public static Empty = BuilderCardEmpty

View File

@ -0,0 +1,47 @@
// Libraries
import React, {PureComponent} from 'react'
import {SelectDropdown} from '@influxdata/clockface'
import {BuilderAggregateFunctionType} from 'src/types'
interface Props {
options: string[]
selectedOption: string
testID: string
onSelect?: (option: BuilderAggregateFunctionType) => void
onDelete?: () => void
}
const emptyFunction = () => {}
export default class BuilderCardDropdownHeader extends PureComponent<Props> {
public static defaultProps = {
testID: 'builder-card--header',
}
public render() {
const {children, options, onSelect, selectedOption, testID} = this.props
return (
<div className="builder-card--header" data-testid={testID}>
<SelectDropdown
options={options}
selectedOption={selectedOption}
testID="select-option-dropdown"
onSelect={onSelect ? onSelect : emptyFunction}
/>
{children}
{this.deleteButton}
</div>
)
}
private get deleteButton(): JSX.Element | undefined {
const {onDelete} = this.props
if (onDelete) {
return <div className="builder-card--delete" onClick={onDelete} />
}
}
}

View File

@ -671,11 +671,16 @@ 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 +766,8 @@ export const timeMachineReducer = (
draftState.queryBuilder.tags[index].values = values
draftState.queryBuilder.tags[index].valuesStatus = RemoteDataState.Done
buildActiveQuery(draftState)
})
}
@ -829,10 +836,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)

View File

@ -1,5 +1,13 @@
// Funcs
import {getStartTime, getEndTime} from 'src/timeMachine/selectors/index'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
import {mocked} from 'ts-jest/utils'
jest.mock('src/shared/utils/featureFlag')
import {
getActiveTagValues,
getStartTime,
getEndTime,
} from 'src/timeMachine/selectors/index'
import moment from 'moment'
import {
@ -59,3 +67,107 @@ describe('TimeMachine.Selectors.Index', () => {
expect(getEndTime(pastThirtyDaysTimeRange)).toBeGreaterThanOrEqual(now)
})
})
describe('getting active tag values', () => {
const activeQueryTags = [
{
keys: [
'_field',
'_measurement',
'cpu',
'device',
'fstype',
'host',
'interface',
'mode',
'name',
'path',
],
values: [
'cpu',
'disk',
'diskio',
'mem',
'net',
'processes',
'swap',
'system',
],
},
{
keys: ['_field', 'host'],
values: [
'active',
'available',
'available_percent',
'buffered',
'cached',
'commit_limit',
'committed_as',
'dirty',
'free',
'high_free',
'high_total',
'huge_page_size',
'huge_pages_free',
'huge_pages_total',
'inactive',
'low_free',
'low_total',
'mapped',
'page_tables',
'shared',
'slab',
'swap_cached',
'swap_free',
'swap_total',
'total',
'used',
'used_percent',
'vmalloc_chunk',
'vmalloc_total',
'vmalloc_used',
'wired',
'write_back',
'write_back_tmp',
],
},
{
keys: ['host'],
values: ['foo_computer'],
},
]
beforeEach(() => {
mocked(isFlagEnabled).mockReset()
})
it("returns the active query tag values when the isFlagEnabled('queryBuilderGrouping') is toggled off", () => {
mocked(isFlagEnabled).mockImplementation(() => {
return false
})
const actualTags = getActiveTagValues(activeQueryTags, 'filter', 2)
expect(actualTags).toEqual(activeQueryTags[2].values)
})
it("returns the active query tag values when the isFlagEnabled('queryBuilderGrouping') is toggled on, but the function is filter", () => {
mocked(isFlagEnabled).mockImplementation(() => {
return true
})
const actualTags = getActiveTagValues(activeQueryTags, 'filter', 2)
expect(actualTags).toEqual(activeQueryTags[2].values)
})
it("returns all previous tag values when the isFlagEnabled('queryBuilderGrouping') is toggled on and the function is group", () => {
mocked(isFlagEnabled).mockImplementation(() => {
return true
})
const actualTags = getActiveTagValues(activeQueryTags, 'group', 2)
expect(actualTags).toEqual([
...activeQueryTags[0].values,
...activeQueryTags[1].values,
])
})
})

View File

@ -22,8 +22,12 @@ import {
durationToMilliseconds,
} from 'src/shared/utils/duration'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
// Types
import {
BuilderAggregateFunctionType,
BuilderTagsType,
DashboardQuery,
FluxTable,
QueryView,
@ -309,3 +313,29 @@ export const getActiveTimeRange = (
}
return null
}
export const getActiveTagValues = (
activeQueryBuilderTags: BuilderTagsType[],
aggregateFunctionType: BuilderAggregateFunctionType,
index: number
): string[] => {
// if we're grouping, we want to be able to group on all previous tags
if (
isFlagEnabled('queryBuilderGrouping') &&
aggregateFunctionType === 'group'
) {
const values = []
activeQueryBuilderTags.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 === index) {
return
}
tag.values.forEach(value => {
values.push(value)
})
})
return values
}
return activeQueryBuilderTags[index].values
}

View File

@ -1,5 +1,10 @@
import {get, isEmpty} from 'lodash'
import {BuilderConfig, DashboardDraftQuery, Check} from 'src/types'
import {
BuilderConfig,
BuilderTagsType,
DashboardDraftQuery,
Check,
} from 'src/types'
import {FUNCTIONS} from 'src/timeMachine/constants/queryBuilder'
import {
TIME_RANGE_START,
@ -8,7 +13,7 @@ import {
WINDOW_PERIOD,
} from 'src/variables/constants'
import {AGG_WINDOW_AUTO} from 'src/timeMachine/constants/queryBuilder'
import {BuilderTagsType} from '@influxdata/influx'
import {isFlagEnabled} from 'src/shared/utils/featureFlag'
export function isConfigValid(builderConfig: BuilderConfig): boolean {
const {buckets, tags} = builderConfig
@ -85,11 +90,14 @@ export function buildQuery(builderConfig: BuilderConfig): string {
const {functions} = builderConfig
let query: string
const helper = isFlagEnabled('queryBuilderGrouping')
? buildQueryHelperButWithGrouping
: buildQueryHelper
if (functions.length) {
query = functions.map(f => buildQueryHelper(builderConfig, f)).join('\n\n')
query = functions.map(f => helper(builderConfig, f)).join('\n\n')
} else {
query = buildQueryHelper(builderConfig)
query = helper(builderConfig, null)
}
return query
@ -110,6 +118,30 @@ function buildQueryHelper(
return query
}
function buildQueryHelperButWithGrouping(
builderConfig: BuilderConfig,
fn?: BuilderConfig['functions'][0]
): string {
const [bucket] = builderConfig.buckets
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})${tagsFunctionCalls}${fnCall}`
return query
}
export function formatFunctionCall(
fn: BuilderConfig['functions'][0],
period: string
@ -125,6 +157,37 @@ export function formatFunctionCall(
return `\n ${fnSpec.flux(formattedPeriod)}\n |> yield(name: "${fn.name}")`
}
const convertTagsToFluxFunctionString = function convertTagsToFluxFunctionString(
tag: BuilderTagsType
) {
if (!tag.key) {
return ''
}
if (tag.aggregateFunctionType === 'filter') {
if (!tag.values.length) {
return ''
}
const fnBody = tag.values
.map(value => `r.${tag.key} == "${value}"`)
.join(' or ')
return `\n |> filter(fn: (r) => ${fnBody})`
}
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 ''
}
const formatPeriod = (period: string): string => {
if (period === AGG_WINDOW_AUTO || !period) {
return `${OPTION_NAME}.${WINDOW_PERIOD}`

View File

@ -88,6 +88,8 @@ export type TableOptions = TableViewProperties['tableOptions']
export {
DashboardQuery,
BuilderAggregateFunctionType,
BuilderTagsType,
BuilderConfig,
ViewProperties,
QueryEditMode,