Implement new visual query builder designs

pull/10616/head
Christopher Henn 2018-12-21 10:31:32 -08:00 committed by Chris Henn
parent 5de0e2e8d2
commit 2770ae5e7b
38 changed files with 1601 additions and 1227 deletions

5
ui/package-lock.json generated
View File

@ -6656,6 +6656,11 @@
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.2.3.tgz",
"integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw=" "integrity": "sha1-0UD6j2FGWb1lQSMwl92qwlzdmRw="
}, },
"immer": {
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-1.9.3.tgz",
"integrity": "sha512-bUyz3fOHGn82V7h4oVgJGmFglZt53JWwSyVNAT4sO0d7IovHLwLuHbh14uYKY0tewFoDcEdiQW7HuL0NsRVziw=="
},
"import-fresh": { "import-fresh": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",

View File

@ -123,6 +123,7 @@
"encoding-down": "^5.0.4", "encoding-down": "^5.0.4",
"fast.js": "^0.1.1", "fast.js": "^0.1.1",
"history": "^3.2", "history": "^3.2",
"immer": "^1.9.3",
"level-js": "^3.0.0", "level-js": "^3.0.0",
"levelup": "^3.1.1", "levelup": "^3.1.1",
"lodash": "^4.3.0", "lodash": "^4.3.0",

View File

@ -9,6 +9,7 @@ import DropdownDivider from 'src/clockface/components/dropdowns/DropdownDivider'
import DropdownItem from 'src/clockface/components/dropdowns/DropdownItem' import DropdownItem from 'src/clockface/components/dropdowns/DropdownItem'
import DropdownButton from 'src/clockface/components/dropdowns/DropdownButton' import DropdownButton from 'src/clockface/components/dropdowns/DropdownButton'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import WaitingText from 'src/shared/components/WaitingText'
// Types // Types
import { import {
@ -124,8 +125,17 @@ class Dropdown extends Component<Props, State> {
const {expanded} = this.state const {expanded} = this.state
const selectedChild = children.find(child => child.props.id === selectedID) const selectedChild = children.find(child => child.props.id === selectedID)
const dropdownLabel = const isLoading = status === ComponentStatus.Loading
(selectedChild && selectedChild.props.children) || titleText
let dropdownLabel
if (isLoading) {
dropdownLabel = <WaitingText text="Loading" />
} else if (selectedChild) {
dropdownLabel = selectedChild.props.children
} else {
dropdownLabel = titleText
}
return ( return (
<DropdownButton <DropdownButton
@ -211,6 +221,14 @@ class Dropdown extends Component<Props, State> {
} }
} }
private get shouldHaveChildren(): boolean {
const {status} = this.props
return (
status === ComponentStatus.Default || status === ComponentStatus.Valid
)
}
private handleItemClick = (value: any): void => { private handleItemClick = (value: any): void => {
const {onChange} = this.props const {onChange} = this.props
onChange(value) onChange(value)
@ -220,7 +238,7 @@ class Dropdown extends Component<Props, State> {
private validateChildCount = (): void => { private validateChildCount = (): void => {
const {children} = this.props const {children} = this.props
if (React.Children.count(children) === 0) { if (this.shouldHaveChildren && React.Children.count(children) === 0) {
throw new Error( throw new Error(
'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.' 'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.'
) )
@ -236,6 +254,7 @@ class Dropdown extends Component<Props, State> {
if ( if (
mode === DropdownMode.Radio && mode === DropdownMode.Radio &&
this.shouldHaveChildren &&
(isUndefined(selectedID) || isNull(selectedID)) (isUndefined(selectedID) || isNull(selectedID))
) { ) {
throw new Error('Dropdowns in Radio mode require a selectedID prop.') throw new Error('Dropdowns in Radio mode require a selectedID prop.')

View File

@ -34,29 +34,51 @@ class DropdownButton extends Component<Props> {
} }
public render() { public render() {
const {onClick, status, children, title} = this.props const {onClick, children, title} = this.props
return ( return (
<button <button
className={this.classname} className={this.classname}
onClick={onClick} onClick={onClick}
disabled={status === ComponentStatus.Disabled} disabled={this.isDisabled}
title={title} title={title}
> >
{this.icon} {this.icon}
<span className="dropdown--selected">{children}</span> <span className="dropdown--selected">{children}</span>
<span className="dropdown--caret icon caret-down" /> {this.caret}
</button> </button>
) )
} }
private get caret(): JSX.Element {
const {active} = this.props
if (active) {
return <span className="dropdown--caret icon caret-up" />
}
return <span className="dropdown--caret icon caret-down" />
}
private get isDisabled(): boolean {
const {status} = this.props
const isDisabled = [
ComponentStatus.Disabled,
ComponentStatus.Loading,
ComponentStatus.Error,
].includes(status)
return isDisabled
}
private get classname(): string { private get classname(): string {
const {status, active, color, size} = this.props const {active, color, size} = this.props
return classnames('dropdown--button button', { return classnames('dropdown--button button', {
'button-stretch': true, 'button-stretch': true,
'button-disabled': this.isDisabled,
[`button-${color}`]: color, [`button-${color}`]: color,
[`button-${size}`]: size, [`button-${size}`]: size,
disabled: status === ComponentStatus.Disabled,
active, active,
}) })
} }

View File

@ -142,8 +142,7 @@ export const query: DashboardQuery = {
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {
buckets: [], buckets: [],
measurements: [], tags: [],
fields: [],
functions: [], functions: [],
}, },
} }

View File

@ -0,0 +1,348 @@
// APIs
import {
QueryBuilderFetcher,
CancellationError,
} from 'src/shared/apis/v2/queryBuilder'
// Utils
import {
getActiveQuerySource,
getActiveQuery,
} from 'src/shared/selectors/timeMachines'
// Types
import {Dispatch} from 'redux-thunk'
import {GetState} from 'src/types/v2'
import {RemoteDataState} from 'src/types'
const fetcher = new QueryBuilderFetcher()
export type Action =
| SetBuilderBucketSelectionAction
| SetBuilderBucketsAction
| SetBuilderBucketsStatusAction
| SetBuilderTagKeysAction
| SetBuilderTagKeysStatusAction
| SetBuilderTagValuesAction
| SetBuilderTagValuesStatusAction
| SetBuilderTagKeySelectionAction
| SetBuilderTagValuesSelectionAction
| AddTagSelectorAction
| RemoveTagSelectorAction
| SelectFunctionAction
interface SetBuilderBucketsStatusAction {
type: 'SET_BUILDER_BUCKETS_STATUS'
payload: {bucketsStatus: RemoteDataState}
}
const setBuilderBucketsStatus = (
bucketsStatus: RemoteDataState
): SetBuilderBucketsStatusAction => ({
type: 'SET_BUILDER_BUCKETS_STATUS',
payload: {bucketsStatus},
})
interface SetBuilderBucketsAction {
type: 'SET_BUILDER_BUCKETS'
payload: {buckets: string[]}
}
const setBuilderBuckets = (buckets: string[]): SetBuilderBucketsAction => ({
type: 'SET_BUILDER_BUCKETS',
payload: {buckets},
})
interface SetBuilderBucketSelectionAction {
type: 'SET_BUILDER_BUCKET_SELECTION'
payload: {bucket: string}
}
const setBuilderBucket = (bucket: string): SetBuilderBucketSelectionAction => ({
type: 'SET_BUILDER_BUCKET_SELECTION',
payload: {bucket},
})
interface SetBuilderTagKeysAction {
type: 'SET_BUILDER_TAG_KEYS'
payload: {index: number; keys: string[]}
}
const setBuilderTagKeys = (
index: number,
keys: string[]
): SetBuilderTagKeysAction => ({
type: 'SET_BUILDER_TAG_KEYS',
payload: {index, keys},
})
interface SetBuilderTagKeysStatusAction {
type: 'SET_BUILDER_TAG_KEYS_STATUS'
payload: {index: number; status: RemoteDataState}
}
const setBuilderTagKeysStatus = (
index: number,
status: RemoteDataState
): SetBuilderTagKeysStatusAction => ({
type: 'SET_BUILDER_TAG_KEYS_STATUS',
payload: {index, status},
})
interface SetBuilderTagValuesAction {
type: 'SET_BUILDER_TAG_VALUES'
payload: {index: number; values: string[]}
}
const setBuilderTagValues = (
index: number,
values: string[]
): SetBuilderTagValuesAction => ({
type: 'SET_BUILDER_TAG_VALUES',
payload: {index, values},
})
interface SetBuilderTagValuesStatusAction {
type: 'SET_BUILDER_TAG_VALUES_STATUS'
payload: {index: number; status: RemoteDataState}
}
const setBuilderTagValuesStatus = (
index: number,
status: RemoteDataState
): SetBuilderTagValuesStatusAction => ({
type: 'SET_BUILDER_TAG_VALUES_STATUS',
payload: {index, status},
})
interface SetBuilderTagKeySelectionAction {
type: 'SET_BUILDER_TAG_KEY_SELECTION'
payload: {index: number; key: string}
}
const setBuilderTagKeySelection = (
index: number,
key: string
): SetBuilderTagKeySelectionAction => ({
type: 'SET_BUILDER_TAG_KEY_SELECTION',
payload: {index, key},
})
interface SetBuilderTagValuesSelectionAction {
type: 'SET_BUILDER_TAG_VALUES_SELECTION'
payload: {index: number; values: string[]}
}
const setBuilderTagValuesSelection = (
index: number,
values: string[]
): SetBuilderTagValuesSelectionAction => ({
type: 'SET_BUILDER_TAG_VALUES_SELECTION',
payload: {index, values},
})
interface AddTagSelectorAction {
type: 'ADD_TAG_SELECTOR'
}
const addTagSelectorSync = (): AddTagSelectorAction => ({
type: 'ADD_TAG_SELECTOR',
})
interface RemoveTagSelectorAction {
type: 'REMOVE_TAG_SELECTOR'
payload: {index: number}
}
const removeTagSelectorSync = (index: number): RemoveTagSelectorAction => ({
type: 'REMOVE_TAG_SELECTOR',
payload: {index},
})
interface SelectFunctionAction {
type: 'SELECT_BUILDER_FUNCTION'
payload: {name: string}
}
export const selectFunction = (name: string): SelectFunctionAction => ({
type: 'SELECT_BUILDER_FUNCTION',
payload: {name},
})
export const loadBuckets = () => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const queryURL = getActiveQuerySource(getState()).links.query
dispatch(setBuilderBucketsStatus(RemoteDataState.Loading))
try {
const buckets = await fetcher.findBuckets(queryURL)
const selectedBucket = getActiveQuery(getState()).builderConfig.buckets[0]
dispatch(setBuilderBuckets(buckets))
if (selectedBucket && buckets.includes(selectedBucket)) {
dispatch(selectBucket(selectedBucket))
} else {
dispatch(selectBucket(buckets[0]))
}
} catch (e) {
if (e instanceof CancellationError) {
return
}
console.error(e)
dispatch(setBuilderBucketsStatus(RemoteDataState.Error))
}
}
export const selectBucket = (bucket: string) => async (
dispatch: Dispatch<Action>
) => {
dispatch(setBuilderBucket(bucket))
dispatch(loadTagSelector(0))
}
export const loadTagSelector = (index: number) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const {buckets, tags} = getActiveQuery(getState()).builderConfig
if (!tags[index] || !buckets[0]) {
return
}
const tagPredicates = tags.slice(0, index)
const queryURL = getActiveQuerySource(getState()).links.query
dispatch(setBuilderTagKeysStatus(index, RemoteDataState.Loading))
try {
const keys = await fetcher.findKeys(
index,
queryURL,
buckets[0],
tagPredicates
)
const {key} = tags[index]
if (!key) {
dispatch(setBuilderTagKeySelection(index, keys[0]))
} else if (!keys.includes(key)) {
// Even if the selected key didn't come back in the results, let it be
// selected anyway
keys.unshift(key)
}
dispatch(setBuilderTagKeys(index, keys))
dispatch(loadTagSelectorValues(index))
} catch (e) {
if (e instanceof CancellationError) {
return
}
console.error(e)
dispatch(setBuilderTagKeysStatus(index, RemoteDataState.Error))
}
}
const loadTagSelectorValues = (index: number) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const {buckets, tags} = getActiveQuery(getState()).builderConfig
const tagPredicates = tags.slice(0, index)
const queryURL = getActiveQuerySource(getState()).links.query
dispatch(setBuilderTagValuesStatus(index, RemoteDataState.Loading))
try {
const key = getActiveQuery(getState()).builderConfig.tags[index].key
const values = await fetcher.findValues(
index,
queryURL,
buckets[0],
tagPredicates,
key
)
const {values: selectedValues} = tags[index]
for (const selectedValue of selectedValues) {
// Even if the selected values didn't come back in the results, let them
// be selected anyway
if (!values.includes(selectedValue)) {
values.unshift(selectedValue)
}
}
dispatch(setBuilderTagValues(index, values))
dispatch(loadTagSelector(index + 1))
} catch (e) {
if (e instanceof CancellationError) {
return
}
console.error(e)
dispatch(setBuilderTagValuesStatus(index, RemoteDataState.Error))
}
}
export const selectTagValue = (index: number, value: string) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const tags = getActiveQuery(getState()).builderConfig.tags
const values = tags[index].values
let newValues: string[]
if (values.includes(value)) {
newValues = values.filter(v => v !== value)
} else {
newValues = [...values, value]
}
dispatch(setBuilderTagValuesSelection(index, newValues))
if (index === tags.length - 1 && newValues.length) {
dispatch(addTagSelector())
} else {
dispatch(loadTagSelector(index + 1))
}
}
export const selectTagKey = (index: number, key: string) => async (
dispatch: Dispatch<Action>
) => {
dispatch(setBuilderTagKeySelection(index, key))
dispatch(loadTagSelectorValues(index))
}
// TODO
export const searchTagValues = (/* index: number, searchTerm: string */) => async () => {}
export const addTagSelector = () => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
dispatch(addTagSelectorSync())
const newIndex = getActiveQuery(getState()).builderConfig.tags.length - 1
dispatch(loadTagSelector(newIndex))
}
export const removeTagSelector = (index: number) => async (
dispatch: Dispatch<Action>
) => {
fetcher.cancelFindValues(index)
fetcher.cancelFindKeys(index)
dispatch(removeTagSelectorSync(index))
dispatch(loadTagSelector(index))
}

View File

@ -1,6 +1,11 @@
// Actions
import {loadBuckets} from 'src/shared/actions/v2/queryBuilder'
// Types // Types
import {Dispatch} from 'redux-thunk'
import {TimeMachineState} from 'src/shared/reducers/v2/timeMachines' import {TimeMachineState} from 'src/shared/reducers/v2/timeMachines'
import {TimeRange, ViewType, BuilderConfig} from 'src/types/v2' import {Action as QueryBuilderAction} from 'src/shared/actions/v2/queryBuilder'
import {TimeRange, ViewType} from 'src/types/v2'
import { import {
Axes, Axes,
DecimalPlaces, DecimalPlaces,
@ -12,6 +17,7 @@ import {TimeMachineTab} from 'src/types/v2/timeMachine'
import {Color} from 'src/types/colors' import {Color} from 'src/types/colors'
export type Action = export type Action =
| QueryBuilderAction
| SetActiveTimeMachineAction | SetActiveTimeMachineAction
| SetActiveTabAction | SetActiveTabAction
| SetNameAction | SetNameAction
@ -34,7 +40,6 @@ export type Action =
| SetYAxisSuffix | SetYAxisSuffix
| SetYAxisBase | SetYAxisBase
| SetYAxisScale | SetYAxisScale
| SetQuerySourceAction
| SetPrefix | SetPrefix
| SetSuffix | SetSuffix
| IncrementSubmitToken | IncrementSubmitToken
@ -44,7 +49,6 @@ export type Action =
| EditActiveQueryAsFluxAction | EditActiveQueryAsFluxAction
| EditActiveQueryAsInfluxQLAction | EditActiveQueryAsInfluxQLAction
| EditActiveQueryWithBuilderAction | EditActiveQueryWithBuilderAction
| BuildQueryAction
| UpdateActiveQueryNameAction | UpdateActiveQueryNameAction
| SetFieldOptionsAction | SetFieldOptionsAction
| SetTableOptionsAction | SetTableOptionsAction
@ -296,16 +300,6 @@ export const setTextThresholdColoring = (): SetTextThresholdColoringAction => ({
type: 'SET_TEXT_THRESHOLD_COLORING', type: 'SET_TEXT_THRESHOLD_COLORING',
}) })
interface SetQuerySourceAction {
type: 'SET_QUERY_SOURCE'
payload: {sourceID: string}
}
export const setQuerySource = (sourceID: string): SetQuerySourceAction => ({
type: 'SET_QUERY_SOURCE',
payload: {sourceID},
})
interface IncrementSubmitToken { interface IncrementSubmitToken {
type: 'INCREMENT_SUBMIT_TOKEN' type: 'INCREMENT_SUBMIT_TOKEN'
} }
@ -343,41 +337,50 @@ interface SetActiveQueryIndexAction {
payload: {activeQueryIndex: number} payload: {activeQueryIndex: number}
} }
export const setActiveQueryIndex = ( export const setActiveQueryIndexSync = (
activeQueryIndex: number activeQueryIndex: number
): SetActiveQueryIndexAction => ({ ): SetActiveQueryIndexAction => ({
type: 'SET_ACTIVE_QUERY_INDEX', type: 'SET_ACTIVE_QUERY_INDEX',
payload: {activeQueryIndex}, payload: {activeQueryIndex},
}) })
export const setActiveQueryIndex = (activeQueryIndex: number) => (
dispatch: Dispatch<Action>
) => {
dispatch(setActiveQueryIndexSync(activeQueryIndex))
dispatch(loadBuckets())
}
interface AddQueryAction { interface AddQueryAction {
type: 'ADD_QUERY' type: 'ADD_QUERY'
} }
export const addQuery = (): AddQueryAction => ({ export const addQuerySync = (): AddQueryAction => ({
type: 'ADD_QUERY', type: 'ADD_QUERY',
}) })
export const addQuery = () => (dispatch: Dispatch<Action>) => {
dispatch(addQuerySync())
dispatch(loadBuckets())
}
interface RemoveQueryAction { interface RemoveQueryAction {
type: 'REMOVE_QUERY' type: 'REMOVE_QUERY'
payload: {queryIndex: number} payload: {queryIndex: number}
} }
export const removeQuery = (queryIndex: number): RemoveQueryAction => ({ export const removeQuerySync = (queryIndex: number): RemoveQueryAction => ({
type: 'REMOVE_QUERY', type: 'REMOVE_QUERY',
payload: {queryIndex}, payload: {queryIndex},
}) })
interface BuildQueryAction { export const removeQuery = (queryIndex: number) => (
type: 'BUILD_QUERY' dispatch: Dispatch<Action>
payload: {builderConfig: BuilderConfig} ) => {
dispatch(removeQuerySync(queryIndex))
dispatch(loadBuckets())
} }
export const buildQuery = (builderConfig: BuilderConfig): BuildQueryAction => ({
type: 'BUILD_QUERY',
payload: {builderConfig},
})
interface UpdateActiveQueryNameAction { interface UpdateActiveQueryNameAction {
type: 'UPDATE_ACTIVE_QUERY_NAME' type: 'UPDATE_ACTIVE_QUERY_NAME'
payload: {queryName: string} payload: {queryName: string}

View File

@ -1,135 +1,81 @@
// Libraries // Libraries
import {get} from 'lodash' import {get} from 'lodash'
import uuid from 'uuid'
// APIs // APIs
import {executeQuery, ExecuteFluxQueryResult} from 'src/shared/apis/v2/query' import {executeQuery, ExecuteFluxQueryResult} from 'src/shared/apis/v2/query'
import {parseResponse} from 'src/shared/parsing/flux/response' import {parseResponse} from 'src/shared/parsing/flux/response'
// Types // Utils
import {InfluxLanguage} from 'src/types/v2' import {formatTagFilterCall} from 'src/shared/utils/queryBuilder'
import {Source} from 'src/api'
export const SEARCH_DURATION = '30d' // Types
import {InfluxLanguage, BuilderConfig} from 'src/types/v2'
export const SEARCH_DURATION = '5m'
export const LIMIT = 200 export const LIMIT = 200
export async function findBuckets( async function findBuckets(url: string): Promise<string[]> {
url: string, const query = `buckets()
sourceType: Source.TypeEnum, |> sort(columns: ["name"])
searchTerm?: string |> limit(n: ${LIMIT})`
) {
if (sourceType === Source.TypeEnum.V1) {
throw new Error('metaqueries not yet implemented for SourceType.V1')
}
const resp = await findBucketsFlux(url, searchTerm) const resp = await executeQuery(url, query, InfluxLanguage.Flux)
const parsed = extractCol(resp, 'name') const parsed = extractCol(resp, 'name')
return parsed return parsed
} }
export async function findMeasurements( async function findKeys(
url: string, url: string,
sourceType: Source.TypeEnum,
bucket: string, bucket: string,
tagsSelections: BuilderConfig['tags'],
searchTerm: string = '' searchTerm: string = ''
): Promise<string[]> { ): Promise<string[]> {
if (sourceType === Source.TypeEnum.V1) { const tagFilters = formatTagFilterCall(tagsSelections)
throw new Error('metaqueries not yet implemented for SourceType.V1') const searchFilter = formatSearchFilterCall(searchTerm)
} const previousKeyFilter = formatTagKeyFilterCall(tagsSelections)
const resp = await findMeasurementsFlux(url, bucket, searchTerm) const query = `from(bucket: "${bucket}")
const parsed = extractCol(resp, '_measurement') |> range(start: -${SEARCH_DURATION})${tagFilters}
|> keys(except: ["_time", "_start", "_stop", "_value"])
|> group()
|> distinct()
|> keep(columns: ["_value"])${searchFilter}${previousKeyFilter}
|> sort()
|> limit(n: ${LIMIT})`
const resp = await executeQuery(url, query, InfluxLanguage.Flux)
const parsed = extractCol(resp, '_value')
return parsed return parsed
} }
export async function findFields( async function findValues(
url: string, url: string,
sourceType: Source.TypeEnum,
bucket: string, bucket: string,
measurements: string[], tagsSelections: BuilderConfig['tags'],
key: string,
searchTerm: string = '' searchTerm: string = ''
): Promise<string[]> { ): Promise<string[]> {
if (sourceType === Source.TypeEnum.V1) { const tagFilters = formatTagFilterCall(tagsSelections)
throw new Error('metaqueries not yet implemented for SourceType.V1') const searchFilter = formatSearchFilterCall(searchTerm)
}
const resp = await findFieldsFlux(url, bucket, measurements, searchTerm) const query = `from(bucket: "${bucket}")
const parsed = extractCol(resp, '_field') |> range(start: -${SEARCH_DURATION})${tagFilters}
|> group(columns: ["${key}"])
|> distinct(column: "${key}")
|> group()
|> keep(columns: ["_value"])${searchFilter}
|> sort()
|> limit(n: ${LIMIT})`
const resp = await executeQuery(url, query, InfluxLanguage.Flux)
const parsed = extractCol(resp, '_value')
return parsed return parsed
} }
function findBucketsFlux(
url: string,
searchTerm: string
): Promise<ExecuteFluxQueryResult> {
let query = 'buckets()'
if (searchTerm) {
query += `
|> filter(fn: (r) => r.name =~ /(?i:${searchTerm})/)`
}
query += `
|> sort(columns: ["name"])
|> limit(n: ${LIMIT})`
return executeQuery(url, query, InfluxLanguage.Flux)
}
function findMeasurementsFlux(
url: string,
bucket: string,
searchTerm: string
): Promise<ExecuteFluxQueryResult> {
let query = `from(bucket: "${bucket}")
|> range(start: -${SEARCH_DURATION})`
if (searchTerm) {
query += `
|> filter(fn: (r) => r._measurement =~ /(?i:${searchTerm})/)`
}
query += `
|> group(columns: ["_measurement"])
|> distinct(column: "_measurement")
|> group()
|> sort(columns: ["_measurement"])
|> limit(n: ${LIMIT})`
return executeQuery(url, query, InfluxLanguage.Flux)
}
function findFieldsFlux(
url: string,
bucket: string,
measurements: string[],
searchTerm: string
): Promise<ExecuteFluxQueryResult> {
const measurementPredicate = measurements
.map(m => `r._measurement == "${m}"`)
.join(' or ')
let query = `from(bucket: "${bucket}")
|> range(start: -${SEARCH_DURATION})
|> filter(fn: (r) => ${measurementPredicate})`
if (searchTerm) {
query += `
|> filter(fn: (r) => r._field =~ /(?i:${searchTerm})/)`
}
query += `
|> group(columns: ["_field"])
|> distinct(column: "_field")
|> group()
|> sort(columns: ["_field"])
|> limit(n: ${LIMIT})`
return executeQuery(url, query, InfluxLanguage.Flux)
}
function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] { function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
const tables = parseResponse(resp.csv) const tables = parseResponse(resp.csv)
const data = get(tables, '0.data', []) const data = get(tables, '0.data', [])
@ -152,3 +98,100 @@ function extractCol(resp: ExecuteFluxQueryResult, colName: string): string[] {
return colValues return colValues
} }
function formatTagKeyFilterCall(tagsSelections: BuilderConfig['tags']) {
const keys = tagsSelections.map(({key}) => key)
if (!keys.length) {
return ''
}
const fnBody = keys.map(key => `r._value != "${key}"`).join(' and ')
return `\n |> filter(fn: (r) => ${fnBody})`
}
function formatSearchFilterCall(searchTerm: string) {
if (!searchTerm) {
return ''
}
return `\n |> filter(fn: (r) => r._value =~ /(?i:${searchTerm})/)`
}
export class CancellationError extends Error {}
export class QueryBuilderFetcher {
private findBucketsToken: string = ''
private findKeysTokens = []
private findValuesTokens = []
public async findBuckets(url: string): Promise<string[]> {
const token = uuid.v4()
this.findBucketsToken = token
const result = await findBuckets(url)
if (token !== this.findBucketsToken) {
throw new CancellationError()
}
return result
}
public async findKeys(
index: number,
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
searchTerm: string = ''
): Promise<string[]> {
const token = uuid.v4()
this.findKeysTokens[index] = token
const result = await findKeys(url, bucket, tagsSelections, searchTerm)
if (token !== this.findKeysTokens[index]) {
throw new CancellationError()
}
return result
}
public cancelFindKeys(index) {
this.findKeysTokens[index] = uuid.v4()
}
public async findValues(
index: number,
url: string,
bucket: string,
tagsSelections: BuilderConfig['tags'],
key: string,
searchTerm: string = ''
): Promise<string[]> {
const token = uuid.v4()
this.findValuesTokens[index] = token
const result = await findValues(
url,
bucket,
tagsSelections,
key,
searchTerm
)
if (token !== this.findValuesTokens[index]) {
throw new CancellationError()
}
return result
}
public cancelFindValues(index) {
this.findValuesTokens[index] = uuid.v4()
}
}

View File

@ -1,73 +0,0 @@
@import "src/style/modules";
.builder-card {
position: relative;
overflow: hidden;
min-height: 150px;
background: $g4-onyx;
border-radius: 4px;
display: flex;
flex-direction: column;
font-size: 13px;
}
.builder-card-search-bar {
margin: 10px;
flex-grow: 0;
flex-shrink: 0;
}
.builder-card--items {
flex: 1 1 0;
overflow: scroll;
}
.builder-card--item {
font-weight: 500;
padding: 5px 15px;
margin-bottom: 1px;
cursor: pointer;
color: $g14-chromium;
font-family: $code-font;
&.selected {
background: $c-pool;
color: $g18-cloud;
}
&.selected:hover {
background: $c-pool;
opacity: 0.9;
}
&:hover {
background: $g4-onyx;
}
}
.builder-card--empty {
position: absolute;
top: 50%;
transform: translate(0, -50%);
color: $g8-storm;
font-weight: 500;
font-size: 16px;
text-transform: uppercase;
text-align: center;
width: 100%;
padding: 20px;
.waiting-text {
text-align: start;
width: 85px;
margin: 0 auto;
}
}
.builder-card-limit-message {
padding: 15px;
color: $g9-mountain;
text-align: center;
font-style: italic;
}

View File

@ -1,119 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
// Components
import BuilderCardSearchBar from 'src/shared/components/BuilderCardSearchBar'
import BuilderCardLimitMessage from 'src/shared/components/BuilderCardLimitMessage'
import WaitingText from 'src/shared/components/WaitingText'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
// Styles
import 'src/shared/components/BuilderCard.scss'
// Types
import {RemoteDataState} from 'src/types'
interface Props {
items: string[]
selectedItems: string[]
onSelectItems: (selection: string[]) => void
onSearch: (searchTerm: string) => Promise<void>
status?: RemoteDataState
emptyText?: string
limitCount?: number
singleSelect?: boolean
}
class BuilderCard extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
status: RemoteDataState.Done,
emptyText: 'None Found',
limitCount: Infinity,
singleSelect: false,
}
public render() {
return <div className="builder-card">{this.body}</div>
}
private get body(): JSX.Element {
const {status, onSearch, emptyText} = this.props
if (status === RemoteDataState.Error) {
return <div className="builder-card--empty">Failed to load</div>
}
if (status === RemoteDataState.NotStarted) {
return <div className="builder-card--empty">{emptyText}</div>
}
return (
<>
<BuilderCardSearchBar onSearch={onSearch} />
{this.items}
</>
)
}
private get items(): JSX.Element {
const {status, items, limitCount} = this.props
if (status === RemoteDataState.Loading) {
return (
<div className="builder-card--empty">
<WaitingText text="Loading" />
</div>
)
}
return (
<>
<div className="builder-card--items">
{items.length ? (
<FancyScrollbar>
{items.map(item => (
<div
key={item}
className={this.itemClass(item)}
onClick={this.handleToggleItem(item)}
>
{item}
</div>
))}
</FancyScrollbar>
) : (
<div className="builder-card--empty">Nothing found</div>
)}
</div>
<BuilderCardLimitMessage
itemCount={items.length}
limitCount={limitCount}
/>
</>
)
}
private itemClass = (item): string => {
if (this.props.selectedItems.includes(item)) {
return 'builder-card--item selected'
}
return 'builder-card--item'
}
private handleToggleItem = (item: string) => (): void => {
const {singleSelect, selectedItems, onSelectItems} = this.props
if (singleSelect && selectedItems[0] === item) {
onSelectItems([])
} else if (singleSelect) {
onSelectItems([item])
} else if (selectedItems.includes(item)) {
onSelectItems(selectedItems.filter(x => x !== item))
} else {
onSelectItems([...selectedItems, item])
}
}
}
export default BuilderCard

View File

@ -1,20 +0,0 @@
import React, {SFC} from 'react'
interface Props {
itemCount: number
limitCount: number
}
const BuilderCardLimitMessage: SFC<Props> = ({itemCount, limitCount}) => {
if (itemCount < limitCount) {
return null
}
return (
<div className="builder-card-limit-message">
Showing first {limitCount} results. Use the search bar to find more.
</div>
)
}
export default BuilderCardLimitMessage

View File

@ -1,63 +0,0 @@
// Libraries
import React, {PureComponent, ChangeEvent} from 'react'
// Components
import {Input, ComponentSize} from 'src/clockface'
// Utils
import DefaultDebouncer, {Debouncer} from 'src/shared/utils/debouncer'
import {CancellationError} from 'src/utils/restartable'
const SEARCH_DEBOUNCE_MS = 350
interface Props {
onSearch: (searchTerm: string) => Promise<void>
}
interface State {
searchTerm: string
}
class BuilderCardSearchBar extends PureComponent<Props, State> {
public state: State = {searchTerm: ''}
private debouncer: Debouncer = new DefaultDebouncer()
public render() {
const {searchTerm} = this.state
return (
<div className="builder-card-search-bar">
<Input
size={ComponentSize.Small}
placeholder="Search..."
value={searchTerm}
onChange={this.handleChange}
/>
</div>
)
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({searchTerm: e.target.value}, () =>
this.debouncer.call(this.emitChange, SEARCH_DEBOUNCE_MS)
)
}
private emitChange = async () => {
const {onSearch} = this.props
const {searchTerm} = this.state
try {
await onSearch(searchTerm)
} catch (e) {
if (e instanceof CancellationError) {
return
}
this.setState({searchTerm: ''})
}
}
}
export default BuilderCardSearchBar

View File

@ -0,0 +1,26 @@
@import "src/style/modules";
.function-selector {
display: flex;
flex-direction: column;
border-radius: 4px;
background: $g4-onyx;
h3 {
margin: 10px 5px 10px 10px;
padding: 5px 0 0 0;
text-transform: uppercase;
color: $g10-wolf;
font-weight: 500;
font-size: 16px;
}
}
.function-selector--search {
margin: 0 10px 10px 10px;
width: initial !important;
}
.function-selector .selector-list {
flex: 1 1 0;
}

View File

@ -0,0 +1,91 @@
// Libraries
import React, {PureComponent, ChangeEvent} from 'react'
import {connect} from 'react-redux'
// Components
import {Input} from 'src/clockface'
import SelectorList from 'src/shared/components/SelectorList'
// Actions
import {selectFunction} from 'src/shared/actions/v2/queryBuilder'
// Utils
import {getActiveQuery} from 'src/shared/selectors/timeMachines'
// Constants
import {FUNCTIONS} from 'src/shared/constants/queryBuilder'
// Styles
import 'src/shared/components/FunctionSelector.scss'
// Types
import {AppState, BuilderConfig} from 'src/types/v2'
const FUNCTION_NAMES = FUNCTIONS.map(f => f.name)
interface StateProps {
selectedFunctions: BuilderConfig['functions']
}
interface DispatchProps {
onSelectFunction: (fnName: string) => void
}
type Props = StateProps & DispatchProps
interface State {
searchTerm: string
}
class FunctionSelector extends PureComponent<Props, State> {
public state: State = {searchTerm: ''}
public render() {
const {onSelectFunction} = this.props
const {searchTerm} = this.state
return (
<div className="function-selector">
<h3>Aggregate Functions</h3>
<Input
customClass={'function-selector--search'}
value={searchTerm}
onChange={this.handleSetSearchTerm}
placeholder="Search functions..."
/>
<SelectorList
items={this.functions}
selectedItems={this.selectedFunctions}
onSelectItem={onSelectFunction}
/>
</div>
)
}
private get functions(): string[] {
return FUNCTION_NAMES.filter(f => f.includes(this.state.searchTerm))
}
private get selectedFunctions(): string[] {
return this.props.selectedFunctions.map(f => f.name)
}
private handleSetSearchTerm = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({searchTerm: e.target.value})
}
}
const mstp = (state: AppState) => {
const selectedFunctions = getActiveQuery(state).builderConfig.functions
return {selectedFunctions}
}
const mdtp = {
onSelectFunction: selectFunction,
}
export default connect<StateProps, DispatchProps>(
mstp,
mdtp
)(FunctionSelector)

View File

@ -0,0 +1,70 @@
// Libraries
import React, {SFC} from 'react'
import {connect} from 'react-redux'
// Components
import {Dropdown, ComponentSize} from 'src/clockface'
// Actions
import {selectBucket} from 'src/shared/actions/v2/queryBuilder'
// Utils
import {
getActiveTimeMachine,
getActiveQuery,
} from 'src/shared/selectors/timeMachines'
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
// Types
import {AppState} from 'src/types/v2'
import {RemoteDataState} from 'src/types'
interface StateProps {
selectedBucket: string
buckets: string[]
bucketsStatus: RemoteDataState
}
interface DispatchProps {
onSelectBucket: (bucket: string) => void
}
interface OwnProps {}
type Props = StateProps & DispatchProps & OwnProps
const QueryBuilderBucketDropdown: SFC<Props> = props => {
const {selectedBucket, buckets, bucketsStatus, onSelectBucket} = props
return (
<Dropdown
selectedID={selectedBucket}
onChange={onSelectBucket}
buttonSize={ComponentSize.Small}
status={toComponentStatus(bucketsStatus)}
>
{buckets.map(bucket => (
<Dropdown.Item key={bucket} id={bucket} value={bucket}>
{bucket}
</Dropdown.Item>
))}
</Dropdown>
)
}
const mstp = (state: AppState) => {
const {buckets, bucketsStatus} = getActiveTimeMachine(state).queryBuilder
const selectedBucket =
getActiveQuery(state).builderConfig.buckets[0] || buckets[0]
return {selectedBucket, buckets, bucketsStatus}
}
const mdtp = {
onSelectBucket: selectBucket as any,
}
export default connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(QueryBuilderBucketDropdown)

View File

@ -0,0 +1,25 @@
@import "src/style/modules";
.selector-list--item {
font-weight: 500;
padding: 5px 15px;
margin-bottom: 1px;
cursor: pointer;
color: $g14-chromium;
font-family: $code-font;
font-size: 13px;
&.selected {
background: $c-pool;
color: $g18-cloud;
}
&.selected:hover {
background: $c-pool;
opacity: 0.9;
}
&:hover {
background: $g5-pepper;
}
}

View File

@ -0,0 +1,37 @@
import React, {SFC} from 'react'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import 'src/shared/components/SelectorList.scss'
interface Props {
items: string[]
selectedItems: string[]
onSelectItem: (item: string) => void
}
const SelectorList: SFC<Props> = props => {
const {items, selectedItems, onSelectItem} = props
return (
<div className="selector-list">
<FancyScrollbar>
{items.map(item => {
const selectedClass = selectedItems.includes(item) ? 'selected' : ''
return (
<div
className={`selector-list--item ${selectedClass}`}
key={item}
onClick={() => onSelectItem(item)}
>
{item}
</div>
)
})}
</FancyScrollbar>
</div>
)
}
export default SelectorList

View File

@ -0,0 +1,53 @@
@import "src/style/modules";
.tag-selector {
min-height: 130px;
background: $g4-onyx;
border-radius: 4px;
padding: 10px 0;
overflow: hidden;
display: flex;
flex-direction: column;
font-size: 13px;
input {
margin-top: 5px;
}
}
.tag-selector--top, .tag-selector--search {
margin: 0 10px;
}
.tag-selector--top {
display: flex;
}
.tag-selector--search {
margin-bottom: 10px;
width: initial !important;
}
button.tag-selector--remove {
margin-left: 3px;
}
.tag-selector .selector-list {
flex: 1 1 0;
}
.tag-selector--empty {
flex: 1 1 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: $g8-storm;
font-weight: 500;
font-size: 16px;
text-transform: uppercase;
text-align: center;
padding: 10px;
}

View File

@ -0,0 +1,233 @@
// Libraries
import React, {PureComponent, ChangeEvent} from 'react'
import {connect} from 'react-redux'
// Components
import {Dropdown, Input, Button, ButtonShape, IconFont} from 'src/clockface'
import WaitingText from 'src/shared/components/WaitingText'
import SelectorList from 'src/shared/components/SelectorList'
// Actions
import {
selectTagKey,
selectTagValue,
searchTagValues,
removeTagSelector,
} from 'src/shared/actions/v2/queryBuilder'
// Utils
import {toComponentStatus} from 'src/shared/utils/toComponentStatus'
import DefaultDebouncer from 'src/shared/utils/debouncer'
import {
getActiveQuery,
getActiveTimeMachine,
} from 'src/shared/selectors/timeMachines'
// Styles
import 'src/shared/components/TagSelector.scss'
// Types
import {AppState} from 'src/types/v2'
import {RemoteDataState} from 'src/types'
const SEARCH_DEBOUNCE_MS = 500
interface StateProps {
emptyText: string
keys: string[]
keysStatus: RemoteDataState
selectedKey: string
values: string[]
valuesStatus: RemoteDataState
selectedValues: string[]
}
interface DispatchProps {
onSelectValue: (index: number, value: string) => void
onSelectTag: (index: number, tag: string) => void
onSearchValues: (index: number, searchTerm: string) => void
onRemoveTagSelector: (index: number) => void
}
interface OwnProps {
index: number
}
type Props = StateProps & DispatchProps & OwnProps
interface State {
searchTerm: string
}
class TagSelector extends PureComponent<Props, State> {
public state: State = {searchTerm: ''}
private debouncer = new DefaultDebouncer()
public render() {
return <div className="tag-selector">{this.body}</div>
}
private get body() {
const {index, keys, keysStatus, selectedKey, emptyText} = this.props
const {searchTerm} = this.state
if (keysStatus === RemoteDataState.NotStarted) {
return <div className="tag-selector--empty">{emptyText}</div>
}
if (keysStatus === RemoteDataState.Loading) {
return (
<div className="tag-selector--empty">
<WaitingText text="Loading tag keys" />
</div>
)
}
if (keysStatus === RemoteDataState.Error) {
return <div className="tag-selector--empty">Failed to load tag keys</div>
}
if (keysStatus === RemoteDataState.Done && !keys.length) {
return <div className="tag-selector--empty">No more tag keys found</div>
}
return (
<>
<div className="tag-selector--top">
<Dropdown
selectedID={selectedKey}
onChange={this.handleSelectTag}
status={toComponentStatus(keysStatus)}
titleText="No Tags Found"
>
{keys.map(key => (
<Dropdown.Item key={key} id={key} value={key}>
{key}
</Dropdown.Item>
))}
</Dropdown>
{index !== 0 && (
<Button
shape={ButtonShape.Square}
icon={IconFont.Remove}
onClick={this.handleRemoveTagSelector}
customClass="tag-selector--remove"
/>
)}
</div>
<Input
value={searchTerm}
placeholder={`Search ${selectedKey} tag values`}
customClass="tag-selector--search"
onChange={this.handleSearch}
/>
{this.values}
</>
)
}
private get values() {
const {selectedKey, values, valuesStatus, selectedValues} = this.props
if (valuesStatus === RemoteDataState.Error) {
return (
<div className="tag-selector--empty">
{`Failed to load tag values for ${selectedKey}`}
</div>
)
}
if (valuesStatus === RemoteDataState.Loading) {
return (
<div className="tag-selector--empty">
<WaitingText text="Loading tag values" />
</div>
)
}
if (valuesStatus === RemoteDataState.Done && !values.length) {
return <div className="tag-selector--empty">Nothing found</div>
}
return (
<SelectorList
items={values}
selectedItems={selectedValues}
onSelectItem={this.handleSelectValue}
/>
)
}
private handleSelectTag = (tag: string): void => {
const {index, onSelectTag} = this.props
onSelectTag(index, tag)
}
private handleSelectValue = (value: string): void => {
const {index, onSelectValue} = this.props
onSelectValue(index, value)
}
private handleRemoveTagSelector = () => {
const {index, onRemoveTagSelector} = this.props
onRemoveTagSelector(index)
}
private handleSearch = (e: ChangeEvent<HTMLInputElement>) => {
const {value} = e.target
this.setState({searchTerm: value}, () => {
this.debouncer.call(this.emitSearch, SEARCH_DEBOUNCE_MS)
})
}
private emitSearch = () => {
const {index, onSearchValues} = this.props
const {searchTerm} = this.state
onSearchValues(index, searchTerm)
}
}
const mstp = (state: AppState, ownProps: OwnProps): StateProps => {
const {keys, keysStatus, values, valuesStatus} = getActiveTimeMachine(
state
).queryBuilder.tags[ownProps.index]
const tags = getActiveQuery(state).builderConfig.tags
const {key: selectedKey, values: selectedValues} = 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`
}
return {
emptyText,
keys,
keysStatus,
selectedKey,
values,
valuesStatus,
selectedValues,
}
}
const mdtp = {
onSelectValue: selectTagValue,
onSelectTag: selectTagKey,
onSearchValues: searchTagValues,
onRemoveTagSelector: removeTagSelector,
}
export default connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(TagSelector)

View File

@ -11,7 +11,7 @@ import TimeMachineVis from 'src/shared/components/TimeMachineVis'
import TimeSeries from 'src/shared/components/TimeSeries' import TimeSeries from 'src/shared/components/TimeSeries'
// Constants // Constants
const INITIAL_RESIZER_HANDLE = 0.6 const INITIAL_RESIZER_HANDLE = 0.5
// Utils // Utils
import {getActiveTimeMachine} from 'src/shared/selectors/timeMachines' import {getActiveTimeMachine} from 'src/shared/selectors/timeMachines'

View File

@ -11,7 +11,6 @@ import {
ComponentSpacer, ComponentSpacer,
Alignment, Alignment,
} from 'src/clockface' } from 'src/clockface'
import TimeMachineSourceDropdown from 'src/shared/components/TimeMachineSourceDropdown'
import TimeMachineRefreshDropdown from 'src/shared/components/TimeMachineRefreshDropdown' import TimeMachineRefreshDropdown from 'src/shared/components/TimeMachineRefreshDropdown'
import ViewTypeDropdown from 'src/shared/components/view_options/ViewTypeDropdown' import ViewTypeDropdown from 'src/shared/components/view_options/ViewTypeDropdown'
@ -56,7 +55,6 @@ class TimeMachineControls extends PureComponent<Props> {
return ( return (
<div className="time-machine--controls"> <div className="time-machine--controls">
<ComponentSpacer align={Alignment.Left}> <ComponentSpacer align={Alignment.Left}>
<TimeMachineSourceDropdown />
<ViewTypeDropdown /> <ViewTypeDropdown />
</ComponentSpacer> </ComponentSpacer>
<ComponentSpacer align={Alignment.Right}> <ComponentSpacer align={Alignment.Right}>

View File

@ -88,7 +88,9 @@ const TimeMachineQueries: SFC<Props> = props => {
</div> </div>
<div className="time-machine-queries--buttons"> <div className="time-machine-queries--buttons">
<TimeMachineQueriesSwitcher /> <TimeMachineQueriesSwitcher />
<SubmitQueryButton queryStatus={queryStatus} /> {activeQuery.editMode !== QueryEditMode.Builder && (
<SubmitQueryButton queryStatus={queryStatus} />
)}
</div> </div>
</div> </div>
<div className="time-machine-queries--body">{queryEditor}</div> <div className="time-machine-queries--body">{queryEditor}</div>

View File

@ -3,7 +3,7 @@ import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
// Components // Components
import {Button, Dropdown} from 'src/clockface' import {Button} from 'src/clockface'
// Actions // Actions
import { import {
@ -13,14 +13,18 @@ import {
} from 'src/shared/actions/v2/timeMachines' } from 'src/shared/actions/v2/timeMachines'
// Utils // Utils
import {getActiveQuery} from 'src/shared/selectors/timeMachines' import {
getActiveQuery,
getActiveQuerySource,
} from 'src/shared/selectors/timeMachines'
import {CONFIRM_LEAVE_ADVANCED_MODE} from 'src/shared/copy/v2' import {CONFIRM_LEAVE_ADVANCED_MODE} from 'src/shared/copy/v2'
// Types // Types
import {AppState, QueryEditMode} from 'src/types/v2' import {AppState, QueryEditMode, Source} from 'src/types/v2'
interface StateProps { interface StateProps {
editMode: QueryEditMode editMode: QueryEditMode
sourceType: Source.TypeEnum
} }
interface DispatchProps { interface DispatchProps {
@ -33,7 +37,7 @@ type Props = StateProps & DispatchProps
class TimeMachineQueriesSwitcher extends PureComponent<Props> { class TimeMachineQueriesSwitcher extends PureComponent<Props> {
public render() { public render() {
const {editMode, onEditAsFlux, onEditAsInfluxQL} = this.props const {editMode, sourceType, onEditAsFlux, onEditAsInfluxQL} = this.props
if (editMode !== QueryEditMode.Builder) { if (editMode !== QueryEditMode.Builder) {
return ( return (
@ -44,21 +48,11 @@ class TimeMachineQueriesSwitcher extends PureComponent<Props> {
) )
} }
return ( if (sourceType === Source.TypeEnum.V1) {
<Dropdown return <Button text="Edit Query As InfluxQL" onClick={onEditAsInfluxQL} />
selectedID="" }
titleText="Edit Query As..."
widthPixels={130} return <Button text="Edit Query As Flux" onClick={onEditAsFlux} />
onChange={this.handleChooseLanguage}
>
<Dropdown.Item id={'influxQL'} value={onEditAsFlux}>
Flux
</Dropdown.Item>
<Dropdown.Item id={'flux'} value={onEditAsInfluxQL}>
InfluxQL
</Dropdown.Item>
</Dropdown>
)
} }
private handleEditWithBuilder = (): void => { private handleEditWithBuilder = (): void => {
@ -68,16 +62,13 @@ class TimeMachineQueriesSwitcher extends PureComponent<Props> {
onEditWithBuilder() onEditWithBuilder()
} }
} }
private handleChooseLanguage = (actionCreator): void => {
actionCreator()
}
} }
const mstp = (state: AppState) => { const mstp = (state: AppState) => {
const editMode = getActiveQuery(state).editMode const editMode = getActiveQuery(state).editMode
const sourceType = getActiveQuerySource(state).type
return {editMode} return {editMode, sourceType}
} }
const mdtp = { const mdtp = {

View File

@ -6,46 +6,55 @@
right: 0; right: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
display: flex;
padding: 10px; padding: 10px;
display: flex;
flex-direction: column;
@include no-user-select(); @include no-user-select();
background: $g3-castle; background: $g3-castle;
} }
.query-builder--panel { .query-builder--buttons {
margin-left: 5px;
height: 100%;
flex: 1 1 0;
display: flex; display: flex;
flex-direction: column; justify-content: flex-start;
&:first-child { > .form--element {
margin-left: 0; width: 200px;
margin-right: 5px;
} }
} }
.query-builder--panel-header { .query-builder--cards {
padding: 10px 0; flex: 1 1 0;
border-radius: 5px 5px 0 0;
text-transform: uppercase;
font-weight: 500;
font-size: 13px;
color: $g8-storm;
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; }
small { .query-builder--tag-selectors {
margin-left: 10px; display: flex;
padding: 2px 4px 2px 3px; flex-wrap: nowrap;
border-radius: 2px; flex: 1 1 0;
background-color: $g4-onyx; height: 100%;
font-style: italic;
font-size: 11px; .tag-selector {
margin-right: 5px;
flex: 0 0 250px;
&:last-child {
margin-right: 0;
}
} }
} }
.builder-card { .query-builder .function-selector {
height: 100%; flex: 0 0 250px;
flex: 1 1 0; margin-left: 10px;
}
button.query-builder--add-tag-selector {
height: 100%;
background-color: $g4-onyx;
border-color: $g4-onyx;
margin-right: 5px;
flex-grow: 0;
flex-shrink: 0;
} }

View File

@ -1,375 +1,85 @@
// Libraries // Libraries
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import {connect} from 'react-redux' import {connect} from 'react-redux'
import {range} from 'lodash'
// Components // Components
import BuilderCard from 'src/shared/components/BuilderCard' import {Form, Button, ButtonShape, IconFont} from 'src/clockface'
import TagSelector from 'src/shared/components/TagSelector'
// APIs import QueryBuilderBucketDropdown from 'src/shared/components/QueryBuilderBucketDropdown'
import { import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
findBuckets, import FunctionSelector from 'src/shared/components/FunctionSelector'
findMeasurements,
findFields,
LIMIT,
} from 'src/shared/apis/v2/queryBuilder'
// Actions // Actions
import {buildQuery} from 'src/shared/actions/v2/timeMachines' import {loadBuckets, addTagSelector} from 'src/shared/actions/v2/queryBuilder'
// Utils // Utils
import {restartable, CancellationError} from 'src/utils/restartable'
import {getActiveQuery} from 'src/shared/selectors/timeMachines' import {getActiveQuery} from 'src/shared/selectors/timeMachines'
import {getSources, getActiveSource} from 'src/sources/selectors'
// Constants
import {FUNCTIONS} from 'src/shared/constants/queryBuilder'
// Styles // Styles
import 'src/shared/components/TimeMachineQueryBuilder.scss' import 'src/shared/components/TimeMachineQueryBuilder.scss'
// Types // Types
import {RemoteDataState} from 'src/types' import {AppState} from 'src/types/v2'
import {AppState, BuilderConfig} from 'src/types/v2'
import {Source} from 'src/api'
const EMPTY_FIELDS_MESSAGE = 'Select at least one bucket and measurement'
const EMPTY_FUNCTIONS_MESSAGE = 'Select at least one bucket and measurement'
const EMPTY_MEASUREMENTS_MESSAGE = 'Select a bucket'
const mergeUnique = (items: string[], selection: string[]) =>
[...new Set([...items, ...selection])].sort()
interface StateProps { interface StateProps {
queryURL: string tagFiltersLength: number
sourceType: Source.TypeEnum
} }
interface DispatchProps { interface DispatchProps {
onBuildQuery: typeof buildQuery onLoadBuckets: () => Promise<void>
onAddTagSelector: () => void
} }
type Props = StateProps & DispatchProps type Props = StateProps & DispatchProps
interface State { interface State {}
buckets: string[]
bucketsStatus: RemoteDataState
bucketsSelection: string[]
measurements: string[]
measurementsStatus: RemoteDataState
measurementsSelection: string[]
fields: string[]
fieldsStatus: RemoteDataState
fieldsSelection: string[]
functions: string[]
functionsStatus: RemoteDataState
functionsSelection: string[]
}
class TimeMachineQueryBuilder extends PureComponent<Props, State> { class TimeMachineQueryBuilder extends PureComponent<Props, State> {
public state: State = {
buckets: [],
bucketsStatus: RemoteDataState.Loading,
bucketsSelection: [],
measurements: [],
measurementsStatus: RemoteDataState.NotStarted,
measurementsSelection: [],
fields: [],
fieldsStatus: RemoteDataState.NotStarted,
fieldsSelection: [],
functions: FUNCTIONS.map(f => f.name),
functionsStatus: RemoteDataState.NotStarted,
functionsSelection: [],
}
private findBuckets = restartable(findBuckets)
private findMeasurements = restartable(findMeasurements)
private findFields = restartable(findFields)
public componentDidMount() { public componentDidMount() {
this.findAndSetBuckets() this.props.onLoadBuckets()
}
public componentDidUpdate(prevProps: Props) {
if (prevProps.queryURL !== this.props.queryURL) {
this.findAndSetBuckets()
}
} }
public render() { public render() {
const { const {tagFiltersLength, onAddTagSelector} = this.props
buckets,
bucketsStatus,
bucketsSelection,
measurements,
measurementsStatus,
measurementsSelection,
fields,
fieldsStatus,
fieldsSelection,
functions,
functionsSelection,
functionsStatus,
} = this.state
return ( return (
<div className="query-builder"> <div className="query-builder">
<div className="query-builder--panel"> <div className="query-builder--buttons">
<div className="query-builder--panel-header">Select a Bucket</div> <Form.Element label="Bucket">
<BuilderCard <QueryBuilderBucketDropdown />
status={bucketsStatus} </Form.Element>
items={buckets}
selectedItems={bucketsSelection}
onSelectItems={this.handleSelectBuckets}
onSearch={this.findAndSetBuckets}
limitCount={LIMIT}
singleSelect={true}
/>
</div> </div>
<div className="query-builder--panel"> <div className="query-builder--cards">
<div className="query-builder--panel-header">Select Measurements</div> <FancyScrollbar>
<BuilderCard <div className="query-builder--tag-selectors">
status={measurementsStatus} {range(tagFiltersLength).map(i => (
items={measurements} <TagSelector key={i} index={i} />
selectedItems={measurementsSelection} ))}
onSelectItems={this.handleSelectMeasurement} <Button
onSearch={this.findAndSetMeasurements} shape={ButtonShape.Square}
emptyText={EMPTY_MEASUREMENTS_MESSAGE} icon={IconFont.Plus}
limitCount={LIMIT} onClick={onAddTagSelector}
/> customClass="query-builder--add-tag-selector"
</div> />
<div className="query-builder--panel"> </div>
<div className="query-builder--panel-header"> </FancyScrollbar>
Select Fields <FunctionSelector />
<small>Optional</small>
</div>
<BuilderCard
status={fieldsStatus}
items={fields}
selectedItems={fieldsSelection}
onSelectItems={this.handleSelectFields}
onSearch={this.findAndSetFields}
emptyText={EMPTY_FIELDS_MESSAGE}
limitCount={LIMIT}
/>
</div>
<div className="query-builder--panel">
<div className="query-builder--panel-header">
Select Functions
<small>Optional</small>
</div>
<BuilderCard
status={functionsStatus}
items={functions}
selectedItems={functionsSelection}
onSelectItems={this.handleSelectFunctions}
onSearch={this.handleSearchFunctions}
emptyText={EMPTY_FUNCTIONS_MESSAGE}
limitCount={LIMIT}
/>
</div> </div>
</div> </div>
) )
} }
private findAndSetBuckets = async (
searchTerm: string = ''
): Promise<void> => {
const {queryURL, sourceType} = this.props
this.setState({bucketsStatus: RemoteDataState.Loading})
try {
const buckets = await this.findBuckets(queryURL, sourceType, searchTerm)
const {bucketsSelection} = this.state
this.setState({
buckets: mergeUnique(buckets, bucketsSelection),
bucketsStatus: RemoteDataState.Done,
})
} catch (e) {
if (e instanceof CancellationError) {
return
}
this.setState({bucketsStatus: RemoteDataState.Error})
}
}
private handleSelectBuckets = (bucketsSelection: string[]) => {
if (bucketsSelection.length) {
this.setState({bucketsSelection}, () => {
this.findAndSetMeasurements()
this.emitConfig()
})
return
}
this.setState(
{
bucketsSelection: [],
measurements: [],
measurementsStatus: RemoteDataState.NotStarted,
measurementsSelection: [],
fields: [],
fieldsStatus: RemoteDataState.NotStarted,
fieldsSelection: [],
functionsStatus: RemoteDataState.NotStarted,
functionsSelection: [],
},
this.emitConfig
)
}
private findAndSetMeasurements = async (
searchTerm: string = ''
): Promise<void> => {
const {queryURL, sourceType} = this.props
const [selectedBucket] = this.state.bucketsSelection
this.setState({measurementsStatus: RemoteDataState.Loading})
try {
const measurements = await this.findMeasurements(
queryURL,
sourceType,
selectedBucket,
searchTerm
)
const {measurementsSelection} = this.state
this.setState({
measurements: mergeUnique(measurements, measurementsSelection),
measurementsStatus: RemoteDataState.Done,
})
} catch (e) {
if (e instanceof CancellationError) {
return
}
this.setState({measurementsStatus: RemoteDataState.Error})
}
}
private handleSelectMeasurement = (measurementsSelection: string[]) => {
if (measurementsSelection.length) {
this.setState(
{
measurementsSelection,
functionsStatus: RemoteDataState.Done,
},
() => {
this.findAndSetFields()
this.emitConfig()
}
)
return
}
this.setState(
{
measurementsSelection: [],
fields: [],
fieldsStatus: RemoteDataState.NotStarted,
fieldsSelection: [],
functionsStatus: RemoteDataState.NotStarted,
functionsSelection: [],
},
this.emitConfig
)
}
private findAndSetFields = async (searchTerm: string = ''): Promise<void> => {
const {queryURL, sourceType} = this.props
const {measurementsSelection} = this.state
const [selectedBucket] = this.state.bucketsSelection
this.setState({fieldsStatus: RemoteDataState.Loading})
try {
const fields = await this.findFields(
queryURL,
sourceType,
selectedBucket,
measurementsSelection,
searchTerm
)
const {fieldsSelection} = this.state
this.setState({
fields: mergeUnique(fields, fieldsSelection),
fieldsStatus: RemoteDataState.Done,
})
} catch (e) {
if (e instanceof CancellationError) {
return
}
this.setState({fieldsStatus: RemoteDataState.Error})
}
}
private handleSelectFields = (fieldsSelection: string[]) => {
this.setState({fieldsSelection}, this.emitConfig)
}
private handleSelectFunctions = (functionsSelection: string[]) => {
this.setState({functionsSelection}, this.emitConfig)
}
private handleSearchFunctions = async (searchTerm: string) => {
const {functionsSelection} = this.state
const functions = FUNCTIONS.map(f => f.name).filter(name =>
name.toLowerCase().includes(searchTerm.toLowerCase())
)
this.setState({functions: mergeUnique(functions, functionsSelection)})
}
private emitConfig = (): void => {
const {onBuildQuery} = this.props
const {
bucketsSelection,
measurementsSelection,
fieldsSelection,
functionsSelection,
} = this.state
const config: BuilderConfig = {
buckets: bucketsSelection,
measurements: measurementsSelection,
fields: fieldsSelection,
functions: functionsSelection,
}
onBuildQuery(config)
}
} }
const mstp = (state: AppState): StateProps => { const mstp = (state: AppState): StateProps => {
const sources = getSources(state) const tagFiltersLength = getActiveQuery(state).builderConfig.tags.length
const activeSource = getActiveSource(state)
const activeQuery = getActiveQuery(state)
let source: Source return {tagFiltersLength}
if (activeQuery.sourceID) {
source = sources.find(source => source.id === activeQuery.sourceID)
} else {
source = activeSource
}
const queryURL = source.links.query
const sourceType = source.type
return {queryURL, sourceType}
} }
const mdtp = { const mdtp = {
onBuildQuery: buildQuery, onLoadBuckets: loadBuckets as any,
onAddTagSelector: addTagSelector,
} }
export default connect<StateProps, DispatchProps>( export default connect<StateProps, DispatchProps>(

View File

@ -1,71 +0,0 @@
// Libraries
import React, {SFC} from 'react'
import {connect} from 'react-redux'
import {get} from 'lodash'
// Components
import {Dropdown, ComponentSize} from 'src/clockface'
// Actions
import {setQuerySource} from 'src/shared/actions/v2/timeMachines'
// Utils
import {getActiveTimeMachine} from 'src/shared/selectors/timeMachines'
import {getSources} from 'src/sources/selectors'
// Types
import {AppState, Source} from 'src/types/v2'
const DYNAMIC_SOURCE = {id: '', name: 'Dynamic Source'}
const DROPDOWN_WIDTH = 200
interface StateProps {
sources: Source[]
selectedSourceID: string | null
}
interface DispatchProps {
onSelectSource: (sourceID: string) => void
}
type Props = StateProps & DispatchProps
const TimeMachineSourceDropdown: SFC<Props> = props => {
const {sources, selectedSourceID, onSelectSource} = props
return (
<Dropdown
selectedID={selectedSourceID}
onChange={onSelectSource}
buttonSize={ComponentSize.Small}
widthPixels={DROPDOWN_WIDTH}
>
{[DYNAMIC_SOURCE, ...sources].map(source => (
<Dropdown.Item key={source.id} id={source.id} value={source.id}>
{source.name}
</Dropdown.Item>
))}
</Dropdown>
)
}
const mstp = (state: AppState) => {
const sources = getSources(state)
const {activeQueryIndex, view} = getActiveTimeMachine(state)
const selectedSourceID: string = get(
view,
`properties.queries.${activeQueryIndex}.sourceID`,
''
)
return {sources, selectedSourceID}
}
const mdtp = {
onSelectSource: setQuerySource,
}
export default connect<StateProps, DispatchProps, {}>(
mstp,
mdtp
)(TimeMachineSourceDropdown)

View File

@ -113,7 +113,7 @@ class CellComponent extends Component<Props> {
} = this.props } = this.props
return ( return (
<Conditional isRendered={viewStatus !== RemoteDataState.Done}> <Conditional isRendered={viewStatus === RemoteDataState.Done}>
<ViewComponent <ViewComponent
view={view} view={view}
onZoom={onZoom} onZoom={onZoom}

View File

@ -1,2 +1,2 @@
export const CONFIRM_LEAVE_ADVANCED_MODE = export const CONFIRM_LEAVE_ADVANCED_MODE =
'You will lose any edits you have made to your query. Are you sure?' 'You will lose any manual edits you have made to your query. Are you sure?'

View File

@ -9,15 +9,14 @@ import {
// Actions // Actions
import { import {
submitScript, submitScript,
setQuerySource,
setActiveTab, setActiveTab,
setActiveTimeMachine, setActiveTimeMachine,
setActiveQueryIndex, setActiveQueryIndexSync,
editActiveQueryWithBuilder, editActiveQueryWithBuilder,
editActiveQueryAsFlux, editActiveQueryAsFlux,
editActiveQueryAsInfluxQL, editActiveQueryAsInfluxQL,
addQuery, addQuerySync,
removeQuery, removeQuerySync,
updateActiveQueryName, updateActiveQueryName,
setBackgroundThresholdColoring, setBackgroundThresholdColoring,
setTextThresholdColoring, setTextThresholdColoring,
@ -80,24 +79,14 @@ describe('timeMachinesReducer', () => {
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '123', sourceID: '123',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '456', sourceID: '456',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
@ -127,12 +116,7 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '123', sourceID: '123',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
} }
const queryB: DashboardQuery = { const queryB: DashboardQuery = {
@ -140,12 +124,7 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '456', sourceID: '456',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
} }
state.view.properties.queries = [queryA, queryB] state.view.properties.queries = [queryA, queryB]
@ -160,18 +139,6 @@ describe('timeMachineReducer', () => {
}) })
}) })
describe('SET_QUERY_SOURCE', () => {
test('replaces the sourceID for the active query', () => {
const state = initialStateHelper()
expect(state.draftQueries[0].sourceID).toEqual('')
const nextState = timeMachineReducer(state, setQuerySource('howdy'))
expect(nextState.draftQueries[0].sourceID).toEqual('howdy')
})
})
describe('EDIT_ACTIVE_QUERY_WITH_BUILDER', () => { describe('EDIT_ACTIVE_QUERY_WITH_BUILDER', () => {
test('changes the activeQueryEditor and editMode for the currently active query', () => { test('changes the activeQueryEditor and editMode for the currently active query', () => {
const state = initialStateHelper() const state = initialStateHelper()
@ -183,24 +150,14 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
@ -213,12 +170,7 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: '', text: '',
@ -227,8 +179,7 @@ describe('timeMachineReducer', () => {
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {
buckets: [], buckets: [],
measurements: [], tags: [{key: '_measurement', values: []}],
fields: [],
functions: [], functions: [],
}, },
}, },
@ -247,24 +198,14 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
@ -277,24 +218,14 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
]) ])
}) })
@ -311,24 +242,14 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
@ -341,24 +262,14 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
]) ])
}) })
@ -376,28 +287,18 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
const nextState = timeMachineReducer(state, setActiveQueryIndex(0)) const nextState = timeMachineReducer(state, setActiveQueryIndexSync(0))
expect(nextState.activeQueryIndex).toEqual(0) expect(nextState.activeQueryIndex).toEqual(0)
}) })
@ -412,28 +313,18 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
const nextState = timeMachineReducer(state, setActiveQueryIndex(0)) const nextState = timeMachineReducer(state, setActiveQueryIndexSync(0))
expect(nextState.activeQueryIndex).toEqual(0) expect(nextState.activeQueryIndex).toEqual(0)
}) })
@ -448,28 +339,18 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'bar', text: 'bar',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
const nextState = timeMachineReducer(state, setActiveQueryIndex(0)) const nextState = timeMachineReducer(state, setActiveQueryIndexSync(0))
expect(nextState.activeQueryIndex).toEqual(0) expect(nextState.activeQueryIndex).toEqual(0)
}) })
@ -487,16 +368,11 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
const nextState = timeMachineReducer(state, addQuery()) const nextState = timeMachineReducer(state, addQuerySync())
expect(nextState.activeQueryIndex).toEqual(1) expect(nextState.activeQueryIndex).toEqual(1)
expect(nextState.draftQueries).toEqual([ expect(nextState.draftQueries).toEqual([
@ -505,12 +381,7 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: '', text: '',
@ -519,8 +390,7 @@ describe('timeMachineReducer', () => {
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {
buckets: [], buckets: [],
measurements: [], tags: [{key: '_measurement', values: []}],
fields: [],
functions: [], functions: [],
}, },
}, },
@ -538,36 +408,21 @@ describe('timeMachineReducer', () => {
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'b', text: 'b',
type: InfluxLanguage.Flux, type: InfluxLanguage.Flux,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
{ {
text: 'c', text: 'c',
type: InfluxLanguage.InfluxQL, type: InfluxLanguage.InfluxQL,
sourceID: '', sourceID: '',
editMode: QueryEditMode.Advanced, editMode: QueryEditMode.Advanced,
builderConfig: { builderConfig: {buckets: [], tags: [], functions: []},
buckets: [],
measurements: [],
fields: [],
functions: [],
},
}, },
] ]
}) })
@ -579,9 +434,8 @@ describe('timeMachineReducer', () => {
state.draftQueries = queries state.draftQueries = queries
state.activeQueryIndex = 1 state.activeQueryIndex = 1
const nextState = timeMachineReducer(state, removeQuery(1)) const nextState = timeMachineReducer(state, removeQuerySync(1))
expect(nextState.view.properties.queries).toEqual(queries)
expect(nextState.draftQueries).toEqual([queries[0], queries[2]]) expect(nextState.draftQueries).toEqual([queries[0], queries[2]])
expect(nextState.activeQueryIndex).toEqual(1) expect(nextState.activeQueryIndex).toEqual(1)
}) })
@ -593,9 +447,8 @@ describe('timeMachineReducer', () => {
state.draftQueries = queries state.draftQueries = queries
state.activeQueryIndex = 2 state.activeQueryIndex = 2
const nextState = timeMachineReducer(state, removeQuery(2)) const nextState = timeMachineReducer(state, removeQuerySync(2))
expect(nextState.view.properties.queries).toEqual(queries)
expect(nextState.draftQueries).toEqual([queries[0], queries[1]]) expect(nextState.draftQueries).toEqual([queries[0], queries[1]])
expect(nextState.activeQueryIndex).toEqual(1) expect(nextState.activeQueryIndex).toEqual(1)
}) })
@ -606,12 +459,7 @@ describe('timeMachineReducer', () => {
const state = initialStateHelper() const state = initialStateHelper()
state.activeQueryIndex = 1 state.activeQueryIndex = 1
const builderConfig = { const builderConfig = {buckets: [], tags: [], functions: []}
buckets: [],
measurements: [],
fields: [],
functions: [],
}
state.draftQueries = [ state.draftQueries = [
{ {

View File

@ -1,5 +1,6 @@
// Libraries // Libraries
import {get, cloneDeep} from 'lodash' import {get, cloneDeep} from 'lodash'
import {produce} from 'immer'
// Utils // Utils
import {createView, defaultViewQuery} from 'src/shared/utils/view' import {createView, defaultViewQuery} from 'src/shared/utils/view'
@ -17,6 +18,7 @@ import {
View, View,
ViewType, ViewType,
DashboardQuery, DashboardQuery,
BuilderConfig,
InfluxLanguage, InfluxLanguage,
QueryEditMode, QueryEditMode,
QueryView, QueryView,
@ -25,6 +27,18 @@ import {
} from 'src/types/v2/dashboards' } from 'src/types/v2/dashboards'
import {Action} from 'src/shared/actions/v2/timeMachines' import {Action} from 'src/shared/actions/v2/timeMachines'
import {TimeMachineTab} from 'src/types/v2/timeMachine' import {TimeMachineTab} from 'src/types/v2/timeMachine'
import {RemoteDataState} from 'src/types'
interface QueryBuilderState {
buckets: string[]
bucketsStatus: RemoteDataState
tags: Array<{
keys: string[]
keysStatus: RemoteDataState
values: string[]
valuesStatus: RemoteDataState
}>
}
export interface TimeMachineState { export interface TimeMachineState {
view: QueryView view: QueryView
@ -34,6 +48,7 @@ export interface TimeMachineState {
activeTab: TimeMachineTab activeTab: TimeMachineTab
activeQueryIndex: number | null activeQueryIndex: number | null
submitToken: number submitToken: number
queryBuilder: QueryBuilderState
} }
export interface TimeMachinesState { export interface TimeMachinesState {
@ -51,6 +66,11 @@ export const initialStateHelper = (): TimeMachineState => ({
activeTab: TimeMachineTab.Queries, activeTab: TimeMachineTab.Queries,
activeQueryIndex: 0, activeQueryIndex: 0,
submitToken: 0, submitToken: 0,
queryBuilder: {
buckets: [],
bucketsStatus: RemoteDataState.NotStarted,
tags: [],
},
}) })
export const initialState = (): TimeMachinesState => ({ export const initialState = (): TimeMachinesState => ({
@ -70,6 +90,7 @@ export const timeMachinesReducer = (
const activeTimeMachine = state.timeMachines[activeTimeMachineID] const activeTimeMachine = state.timeMachines[activeTimeMachineID]
const view = initialState.view || activeTimeMachine.view const view = initialState.view || activeTimeMachine.view
const draftQueries = cloneDeep(view.properties.queries) const draftQueries = cloneDeep(view.properties.queries)
const queryBuilder = initialQueryBuilderState(draftQueries[0].builderConfig)
const activeQueryIndex = 0 const activeQueryIndex = 0
return { return {
@ -83,6 +104,7 @@ export const timeMachinesReducer = (
activeTab: TimeMachineTab.Queries, activeTab: TimeMachineTab.Queries,
activeQueryIndex, activeQueryIndex,
draftQueries, draftQueries,
queryBuilder,
}, },
}, },
} }
@ -122,32 +144,11 @@ export const timeMachineReducer = (
} }
case 'SET_TIME_RANGE': { case 'SET_TIME_RANGE': {
const {timeRange} = action.payload return produce(state, draftState => {
const {view} = state draftState.timeRange = action.payload.timeRange
const rebuildConfig = query => ({ buildAndSubmitAllQueries(draftState)
...query,
text: buildQuery(query.builderConfig, timeRange.duration),
}) })
const draftQueries = state.draftQueries.map(rebuildConfig)
const queries = view.properties.queries.map(rebuildConfig)
const newView = {
...view,
properties: {
...view.properties,
queries,
},
}
return {
...state,
timeRange,
view: newView,
draftQueries,
submitToken: Date.now(),
}
} }
case 'SET_VIEW_TYPE': { case 'SET_VIEW_TYPE': {
@ -170,19 +171,9 @@ export const timeMachineReducer = (
} }
case 'SUBMIT_SCRIPT': { case 'SUBMIT_SCRIPT': {
const {view, draftQueries} = state return produce(state, draftState => {
submitQueries(draftState)
return { })
...state,
submitToken: Date.now(),
view: {
...view,
properties: {
...view.properties,
queries: draftQueries,
},
},
}
} }
case 'SET_IS_VIEWING_RAW_DATA': { case 'SET_IS_VIEWING_RAW_DATA': {
@ -342,20 +333,6 @@ export const timeMachineReducer = (
return setViewProperties(state, {staticLegend}) return setViewProperties(state, {staticLegend})
} }
case 'SET_QUERY_SOURCE': {
const {sourceID} = action.payload
const {activeQueryIndex} = state
const draftQueries = [...state.draftQueries]
draftQueries[activeQueryIndex] = {
...draftQueries[activeQueryIndex],
sourceID,
}
return {...state, draftQueries}
}
case 'INCREMENT_SUBMIT_TOKEN': { case 'INCREMENT_SUBMIT_TOKEN': {
return { return {
...state, ...state,
@ -408,59 +385,186 @@ export const timeMachineReducer = (
} }
case 'SET_ACTIVE_QUERY_INDEX': { case 'SET_ACTIVE_QUERY_INDEX': {
const {activeQueryIndex} = action.payload return produce(state, draftState => {
const {activeQueryIndex} = action.payload
return {...state, activeQueryIndex} draftState.activeQueryIndex = activeQueryIndex
resetBuilderState(draftState)
})
} }
case 'ADD_QUERY': { case 'ADD_QUERY': {
const draftQueries = [...state.draftQueries, defaultViewQuery()] return produce(state, draftState => {
const activeQueryIndex: number = draftQueries.length - 1 draftState.draftQueries = [...state.draftQueries, defaultViewQuery()]
draftState.activeQueryIndex = draftState.draftQueries.length - 1
return {...state, activeQueryIndex, draftQueries} resetBuilderState(draftState)
})
} }
case 'REMOVE_QUERY': { case 'REMOVE_QUERY': {
const {queryIndex} = action.payload return produce(state, draftState => {
const draftQueries = state.draftQueries.filter( const {queryIndex} = action.payload
(__, i) => i !== queryIndex
)
const queryLength = draftQueries.length
let activeQueryIndex: number draftState.draftQueries.splice(queryIndex, 1)
if (queryIndex < queryLength) { const queryLength = draftState.draftQueries.length
activeQueryIndex = queryIndex
} else if (queryLength === queryIndex && queryLength > 0) {
activeQueryIndex = queryLength - 1
} else {
activeQueryIndex = 0
}
return {...state, activeQueryIndex, draftQueries} let activeQueryIndex: number
if (queryIndex < queryLength) {
activeQueryIndex = queryIndex
} else if (queryLength === queryIndex && queryLength > 0) {
activeQueryIndex = queryLength - 1
} else {
activeQueryIndex = 0
}
draftState.activeQueryIndex = activeQueryIndex
resetBuilderState(draftState)
submitQueries(draftState)
})
} }
case 'BUILD_QUERY': { case 'SET_BUILDER_BUCKET_SELECTION': {
const {builderConfig} = action.payload return produce(state, draftState => {
const {activeQueryIndex, timeRange} = state const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
const draftQueries = [...state.draftQueries]
let text: string draftQuery.builderConfig.buckets = [action.payload.bucket]
})
if (!isConfigValid(builderConfig)) {
text = ''
} else {
text = buildQuery(builderConfig, timeRange.duration)
}
draftQueries[activeQueryIndex] = {
...draftQueries[activeQueryIndex],
text,
builderConfig,
}
return {...state, draftQueries}
} }
case 'SET_BUILDER_BUCKETS': {
return produce(state, draftState => {
draftState.queryBuilder.buckets = action.payload.buckets
draftState.queryBuilder.bucketsStatus = RemoteDataState.Done
})
}
case 'SET_BUILDER_BUCKETS_STATUS': {
return produce(state, draftState => {
draftState.queryBuilder.bucketsStatus = action.payload.bucketsStatus
})
}
case 'SET_BUILDER_TAG_KEYS': {
return produce(state, draftState => {
const {index, keys} = action.payload
draftState.queryBuilder.tags[index].keys = keys
draftState.queryBuilder.tags[index].keysStatus = RemoteDataState.Done
})
}
case 'SET_BUILDER_TAG_KEYS_STATUS': {
return produce(state, draftState => {
const {index, status} = action.payload
const tags = draftState.queryBuilder.tags
tags[index].keysStatus = status
if (status === RemoteDataState.Loading) {
for (let i = index + 1; i < tags.length; i++) {
tags[i].keysStatus = RemoteDataState.NotStarted
}
}
})
}
case 'SET_BUILDER_TAG_VALUES': {
return produce(state, draftState => {
const {index, values} = action.payload
draftState.queryBuilder.tags[index].values = values
draftState.queryBuilder.tags[index].valuesStatus = RemoteDataState.Done
})
}
case 'SET_BUILDER_TAG_VALUES_STATUS': {
return produce(state, draftState => {
const {index, status} = action.payload
draftState.queryBuilder.tags[index].valuesStatus = status
})
}
case 'SET_BUILDER_TAG_KEY_SELECTION': {
return produce(state, draftState => {
const {index, key} = action.payload
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
const tag = draftQuery.builderConfig.tags[index]
tag.key = key
tag.values = []
})
}
case 'SET_BUILDER_TAG_VALUES_SELECTION': {
return produce(state, draftState => {
const {index, values} = action.payload
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
draftQuery.builderConfig.tags[index].values = values
buildAndSubmitActiveQuery(draftState)
})
}
case 'ADD_TAG_SELECTOR': {
return produce(state, draftState => {
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
draftQuery.builderConfig.tags.push({key: '', values: []})
draftState.queryBuilder.tags.push({
keys: [],
keysStatus: RemoteDataState.NotStarted,
values: [],
valuesStatus: RemoteDataState.NotStarted,
})
})
}
case 'REMOVE_TAG_SELECTOR': {
return produce(state, draftState => {
const {index} = action.payload
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
const selectedValues = draftQuery.builderConfig.tags[index].values
draftQuery.builderConfig.tags.splice(index, 1)
draftState.queryBuilder.tags.splice(index, 1)
if (selectedValues.length) {
buildAndSubmitActiveQuery(draftState)
}
})
}
case 'SELECT_BUILDER_FUNCTION': {
return produce(state, draftState => {
const {name} = action.payload
const functions =
draftState.draftQueries[draftState.activeQueryIndex].builderConfig
.functions
let newFunctions
if (functions.find(f => f.name === name)) {
newFunctions = functions.filter(f => f.name !== name)
} else {
newFunctions = [...functions, {name}]
}
draftState.draftQueries[
draftState.activeQueryIndex
].builderConfig.functions = newFunctions
buildAndSubmitActiveQuery(draftState)
})
}
case 'UPDATE_ACTIVE_QUERY_NAME': { case 'UPDATE_ACTIVE_QUERY_NAME': {
const {activeQueryIndex} = state const {activeQueryIndex} = state
const {queryName} = action.payload const {queryName} = action.payload
@ -484,6 +588,7 @@ export const timeMachineReducer = (
return {...state, view} return {...state, view}
} }
case 'SET_TABLE_OPTIONS': { case 'SET_TABLE_OPTIONS': {
const workingView = state.view as ExtractWorkingView< const workingView = state.view as ExtractWorkingView<
typeof action.payload typeof action.payload
@ -549,3 +654,62 @@ const convertView = (
return newView return newView
} }
const initialQueryBuilderState = (
builderConfig: BuilderConfig
): QueryBuilderState => {
return {
buckets: builderConfig.buckets,
bucketsStatus: RemoteDataState.NotStarted,
tags: builderConfig.tags.map(_ => ({
keys: [],
keysStatus: RemoteDataState.NotStarted,
values: [],
valuesStatus: RemoteDataState.NotStarted,
})),
}
}
const buildAndSubmitActiveQuery = (draftState: TimeMachineState) => {
const draftQuery = draftState.draftQueries[draftState.activeQueryIndex]
if (isConfigValid(draftQuery.builderConfig)) {
draftQuery.text = buildQuery(
draftQuery.builderConfig,
draftState.timeRange.duration
)
} else {
draftQuery.text = ''
}
submitQueries(draftState)
}
const buildAndSubmitAllQueries = (draftState: TimeMachineState) => {
draftState.draftQueries
.filter(query => query.editMode === QueryEditMode.Builder)
.forEach(query => {
if (isConfigValid(query.builderConfig)) {
query.text = buildQuery(
query.builderConfig,
draftState.timeRange.duration
)
} else {
query.text = ''
}
})
submitQueries(draftState)
}
const resetBuilderState = (draftState: TimeMachineState) => {
const newBuilderConfig =
draftState.draftQueries[draftState.activeQueryIndex].builderConfig
draftState.queryBuilder = initialQueryBuilderState(newBuilderConfig)
}
const submitQueries = (draftState: TimeMachineState) => {
draftState.submitToken = Date.now()
draftState.view.properties.queries = draftState.draftQueries
}

View File

@ -1,4 +1,6 @@
import {AppState, DashboardQuery} from 'src/types/v2' import {AppState, DashboardQuery, Source} from 'src/types/v2'
import {getSources} from 'src/sources/selectors'
export const getActiveTimeMachine = (state: AppState) => { export const getActiveTimeMachine = (state: AppState) => {
const {activeTimeMachineID, timeMachines} = state.timeMachines const {activeTimeMachineID, timeMachines} = state.timeMachines
@ -12,3 +14,9 @@ export const getActiveQuery = (state: AppState): DashboardQuery => {
return draftQueries[activeQueryIndex] return draftQueries[activeQueryIndex]
} }
export const getActiveQuerySource = (state: AppState): Source => {
// We only support the self source for now, but in the future the active
// query may be using some other source or the “dynamic source”
return getSources(state).find(s => s.type === Source.TypeEnum.Self)
}

View File

@ -3,11 +3,10 @@ import {buildQuery} from 'src/shared/utils/queryBuilder'
import {BuilderConfig} from 'src/types/v2' import {BuilderConfig} from 'src/types/v2'
describe('buildQuery', () => { describe('buildQuery', () => {
test('single bucket, single measurement', () => { test('single tag', () => {
const config: BuilderConfig = { const config: BuilderConfig = {
buckets: ['b0'], buckets: ['b0'],
measurements: ['m0'], tags: [{key: '_measurement', values: ['m0']}],
fields: [],
functions: [], functions: [],
} }
@ -20,11 +19,13 @@ describe('buildQuery', () => {
expect(actual).toEqual(expected) expect(actual).toEqual(expected)
}) })
test('single bucket, multiple measurements, multiple fields', () => { test('multiple tags', () => {
const config: BuilderConfig = { const config: BuilderConfig = {
buckets: ['b0'], buckets: ['b0'],
measurements: ['m0', 'm1'], tags: [
fields: ['f0', 'f1'], {key: '_measurement', values: ['m0', 'm1']},
{key: '_field', values: ['f0', 'f1']},
],
functions: [], functions: [],
} }
@ -38,12 +39,11 @@ describe('buildQuery', () => {
expect(actual).toEqual(expected) expect(actual).toEqual(expected)
}) })
test('multiple buckets, single measurement, multiple functions', () => { test('single tag, multiple functions', () => {
const config: BuilderConfig = { const config: BuilderConfig = {
buckets: ['b0', 'b1'], buckets: ['b0'],
measurements: ['m0'], tags: [{key: '_measurement', values: ['m0']}],
fields: [], functions: [{name: 'mean'}, {name: 'median'}],
functions: ['mean', 'median'],
} }
const expected = `from(bucket: "b0") const expected = `from(bucket: "b0")
@ -55,23 +55,6 @@ describe('buildQuery', () => {
|> yield(name: "mean") |> yield(name: "mean")
from(bucket: "b0") from(bucket: "b0")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "m0")
|> window(every: 1m)
|> toFloat()
|> median()
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|> yield(name: "median")
from(bucket: "b1")
|> range(start: -1h)
|> filter(fn: (r) => r._measurement == "m0")
|> window(every: 1m)
|> mean()
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|> yield(name: "mean")
from(bucket: "b1")
|> range(start: -1h) |> range(start: -1h)
|> filter(fn: (r) => r._measurement == "m0") |> filter(fn: (r) => r._measurement == "m0")
|> window(every: 1m) |> window(every: 1m)

View File

@ -28,8 +28,11 @@ export const timeRangeVariables = (
} }
export function isConfigValid(builderConfig: BuilderConfig): boolean { export function isConfigValid(builderConfig: BuilderConfig): boolean {
const {buckets, measurements} = builderConfig const {buckets, tags} = builderConfig
const isConfigValid = buckets.length >= 1 && measurements.length >= 1 const isConfigValid =
buckets.length >= 1 &&
tags.length >= 1 &&
tags.some(({key, values}) => key && values.length > 0)
return isConfigValid return isConfigValid
} }
@ -38,64 +41,72 @@ export function buildQuery(
builderConfig: BuilderConfig, builderConfig: BuilderConfig,
duration: string = '1h' duration: string = '1h'
): string { ): string {
const {buckets, measurements, fields, functions} = builderConfig const {functions} = builderConfig
let bucketFunctionPairs: Array<[string, string]> let query: string
if (functions.length) { if (functions.length) {
bucketFunctionPairs = [].concat( query = functions
...buckets.map(b => functions.map(f => [b, f])) .map(f => buildQueryHelper(builderConfig, duration, f))
) .join('\n\n')
} else { } else {
bucketFunctionPairs = buckets.map(b => [b, null] as [string, string]) query = buildQueryHelper(builderConfig, duration)
} }
const query = bucketFunctionPairs
.map(([b, f]) => buildQueryHelper(b, measurements, fields, f, duration))
.join('\n\n')
return query return query
} }
function buildQueryHelper( function buildQueryHelper(
bucket: string, builderConfig: BuilderConfig,
measurements: string[], duration: string,
fields: string[], fn?: BuilderConfig['functions'][0]
functionName: string,
duration: string
): string { ): string {
let query = `from(bucket: "${bucket}") const [bucket] = builderConfig.buckets
|> range(start: -${duration})` const tagFilterCall = formatTagFilterCall(builderConfig.tags)
const fnCall = fn ? formatFunctionCall(fn, duration) : ''
if (measurements.length) { const query = `from(bucket: "${bucket}")
const measurementsPredicate = measurements |> range(start: -${duration})${tagFilterCall}${fnCall}`
.map(m => `r._measurement == "${m}"`)
.join(' or ')
query += `
|> filter(fn: (r) => ${measurementsPredicate})`
}
if (fields.length) {
const fieldsPredicate = fields.map(f => `r._field == "${f}"`).join(' or ')
query += `
|> filter(fn: (r) => ${fieldsPredicate})`
}
const fn = FUNCTIONS.find(f => f.name === functionName)
if (fn && fn.aggregate) {
query += `
|> window(every: ${WINDOW_INTERVALS[duration] || DEFAULT_WINDOW_INTERVAL})
${fn.flux}
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|> yield(name: "${fn.name}")`
} else if (fn) {
query += `
${fn.flux}
|> yield(name: "${fn.name}")`
}
return query return query
} }
export function formatFunctionCall(
fn: BuilderConfig['functions'][0],
duration: string
) {
const fnSpec = FUNCTIONS.find(f => f.name === fn.name)
let fnCall: string = ''
if (fnSpec && fnSpec.aggregate) {
fnCall = `
|> window(every: ${WINDOW_INTERVALS[duration] || DEFAULT_WINDOW_INTERVAL})
${fnSpec.flux}
|> group(columns: ["_value", "_time", "_start", "_stop"], mode: "except")
|> yield(name: "${fn.name}")`
} else {
fnCall = `
${fnSpec.flux}
|> yield(name: "${fn.name}")`
}
return fnCall
}
export function formatTagFilterCall(tagsSelections: BuilderConfig['tags']) {
if (!tagsSelections.length) {
return ''
}
const calls = tagsSelections
.filter(({key, values}) => key && values.length)
.map(({key, values}) => {
const fnBody = values.map(value => `r.${key} == "${value}"`).join(' or ')
return `|> filter(fn: (r) => ${fnBody})`
})
.join('\n ')
return `\n ${calls}`
}

View File

@ -0,0 +1,18 @@
import {ComponentStatus} from 'src/clockface'
import {RemoteDataState} from 'src/types'
export const toComponentStatus = (status: RemoteDataState): ComponentStatus => {
if (status === RemoteDataState.NotStarted) {
return ComponentStatus.Disabled
}
if (status === RemoteDataState.Loading) {
return ComponentStatus.Loading
}
if (status === RemoteDataState.Error) {
return ComponentStatus.Error
}
return ComponentStatus.Default
}

View File

@ -33,8 +33,7 @@ export function defaultViewQuery(): DashboardQuery {
editMode: QueryEditMode.Builder, editMode: QueryEditMode.Builder,
builderConfig: { builderConfig: {
buckets: [], buckets: [],
measurements: [], tags: [{key: '_measurement', values: []}],
fields: [],
functions: [], functions: [],
}, },
} }

View File

@ -48,9 +48,8 @@ export enum QueryEditMode {
export interface BuilderConfig { export interface BuilderConfig {
buckets: string[] buckets: string[]
measurements: string[] tags: Array<{key: string; values: string[]}>
fields: string[] functions: Array<{name: string}>
functions: string[]
} }
export interface DashboardQuery { export interface DashboardQuery {

View File

@ -23,6 +23,7 @@
], ],
"no-empty": false, "no-empty": false,
"jsx-no-lambda": false, "jsx-no-lambda": false,
"max-classes-per-file": false,
"no-empty-interface": false, "no-empty-interface": false,
"no-shadowed-variable": false, "no-shadowed-variable": false,
"prefer-for-of": false, "prefer-for-of": false,

12
view.go
View File

@ -428,10 +428,14 @@ type DashboardQuery struct {
} }
type BuilderConfig struct { type BuilderConfig struct {
Buckets []string `json:"buckets"` Buckets []string `json:"buckets"`
Measurements []string `json:"measurements"` Tags []struct {
Fields []string `json:"fields"` Key string `json:"key"`
Functions []string `json:"functions"` Values []string `json:"values"`
} `json:"tags"`
Functions []struct {
Name string `json:"name"`
} `json:"functions"`
} }
// Axis represents the visible extents of a visualization // Axis represents the visible extents of a visualization