Merge pull request #3734 from influxdata/tempVars/meta-queries

Add "Custom Meta Query" template variable type
pull/10616/head
Chris Henn 2018-06-20 10:38:29 -07:00 committed by GitHub
commit c81658f938
12 changed files with 324 additions and 44 deletions

View File

@ -159,7 +159,7 @@ type Range struct {
// TemplateValue is a value use to replace a template in an InfluxQL query
type TemplateValue struct {
Value string `json:"value"` // Value is the specific value used to replace a template in an InfluxQL query
Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant
Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant, influxql
Selected bool `json:"selected"` // Selected states that this variable has been picked to use for replacement
Key string `json:"key,omitempty"` // Key is the key for the Value if the Template Type is 'map'
}
@ -177,7 +177,7 @@ type TemplateID string
type Template struct {
TemplateVar
ID TemplateID `json:"id"` // ID is the unique ID associated with this template
Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, query, measurements, databases, map
Type string `json:"type"` // Type can be fieldKeys, tagKeys, tagValues, CSV, constant, measurements, databases, map, influxql
Label string `json:"label"` // Label is a user-facing description of the Template
Query *TemplateQuery `json:"query,omitempty"` // Query is used to generate the choices for a template
}

View File

@ -72,7 +72,7 @@ func RenderTemplate(query string, t chronograf.TemplateVar, now time.Time) (stri
return strings.Replace(q, t.Var, `"`+t.Values[0].Value+`"`, -1), nil
case "tagValue", "timeStamp":
return strings.Replace(q, t.Var, `'`+t.Values[0].Value+`'`, -1), nil
case "csv", "constant":
case "csv", "constant", "influxql":
return strings.Replace(q, t.Var, t.Values[0].Value, -1), nil
}

View File

@ -15,14 +15,14 @@ func ValidTemplateRequest(template *chronograf.Template) error {
switch template.Type {
default:
return fmt.Errorf("Unknown template type %s", template.Type)
case "query", "constant", "csv", "fieldKeys", "tagKeys", "tagValues", "measurements", "databases", "map":
case "constant", "csv", "fieldKeys", "tagKeys", "tagValues", "measurements", "databases", "map", "influxql":
}
for _, v := range template.Values {
switch v.Type {
default:
return fmt.Errorf("Unknown template variable type %s", v.Type)
case "csv", "fieldKey", "tagKey", "tagValue", "measurement", "database", "constant":
case "csv", "fieldKey", "tagKey", "tagValue", "measurement", "database", "constant", "influxql":
}
if template.Type == "map" && v.Key == "" {
@ -30,8 +30,8 @@ func ValidTemplateRequest(template *chronograf.Template) error {
}
}
if template.Type == "query" && template.Query == nil {
return fmt.Errorf("No query set for template of type 'query'")
if template.Type == "influxql" && template.Query == nil {
return fmt.Errorf("No query set for template of type 'influxql'")
}
return nil

View File

@ -57,7 +57,7 @@ func TestValidTemplateRequest(t *testing.T) {
name: "No query set",
wantErr: true,
template: &chronograf.Template{
Type: "query",
Type: "influxql",
},
},
{

View File

@ -5,7 +5,9 @@ import {replace} from 'react-router-redux'
import _ from 'lodash'
import queryString from 'query-string'
import {proxy} from 'src/utils/queryUrlGenerator'
import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import {parseMetaQuery} from 'src/tempVars/utils/parsing'
import {
getDashboards as getDashboardsAJAX,
@ -15,7 +17,6 @@ import {
updateDashboardCell as updateDashboardCellAJAX,
addDashboardCell as addDashboardCellAJAX,
deleteDashboardCell as deleteDashboardCellAJAX,
getTempVarValuesBySourceQuery,
createDashboard as createDashboardAJAX,
} from 'src/dashboards/apis'
import {getMe} from 'src/shared/apis/auth'
@ -48,7 +49,6 @@ import {
} from 'src/shared/copy/notifications'
import {makeQueryForTemplate} from 'src/dashboards/utils/tempVars'
import parsers from 'src/shared/parsing'
import {getDeep} from 'src/utils/wrappers'
import idNormalizer, {TYPE_ID} from 'src/normalizers/id'
@ -628,25 +628,23 @@ export const hydrateTempVarValuesAsync = (
const dashboard = getState().dashboardUI.dashboards.find(
d => d.id === dashboardID
)
const templates: Template[] = dashboard.templates
const queries = templates
.filter(
template => getDeep<string>(template, 'query.influxql', '') !== ''
)
.map(async template => {
const query = makeQueryForTemplate(template.query)
const response = await proxy({source: source.links.proxy, query})
const values = parseMetaQuery(query, response.data)
const tempsWithQueries = dashboard.templates.filter(
({query}) => !!query.influxql
)
const asyncQueries = tempsWithQueries.map(({query}) =>
getTempVarValuesBySourceQuery(source, {
query: makeQueryForTemplate(query),
return {template, values}
})
)
const results = await Promise.all(queries)
const results = await Promise.all(asyncQueries)
results.forEach(({data}, i) => {
const {type, query, id} = tempsWithQueries[i]
const parsed = parsers[type](data, query.tagKey || query.measurement)
const vals = parsed[type]
dispatch(editTemplateVariableValues(+dashboard.id, id, vals))
})
for (const {template, values} of results) {
dispatch(editTemplateVariableValues(+dashboard.id, template.id, values))
}
} catch (error) {
console.error(error)
dispatch(errorThrown(error))

View File

@ -1,5 +1,4 @@
import AJAX from 'utils/ajax'
import {proxy} from 'utils/queryUrlGenerator'
export function getDashboards() {
return AJAX({
@ -98,19 +97,3 @@ export const editTemplateVariables = async templateVariable => {
throw error
}
}
export const getTempVarValuesBySourceQuery = async (source, templateQuery) => {
const {
query,
db,
// rp, TODO
tempVars,
} = templateQuery
try {
// TODO: add rp as argument to proxy
return await proxy({source: source.links.proxy, query, db, tempVars})
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -0,0 +1,26 @@
import {getDeep} from 'src/utils/wrappers'
interface ParseShowSeriesResponse {
errors: string[]
series: string[]
}
const parseShowSeries = (response): ParseShowSeriesResponse => {
const results = response.results[0]
if (results.error) {
return {errors: [results.error], series: []}
}
const seriesValues = getDeep<string[]>(results, 'series.0.values', [])
if (!seriesValues.length) {
return {errors: [], series: []}
}
const series = seriesValues.map(s => s[0])
return {series, errors: []}
}
export default parseShowSeries

View File

@ -0,0 +1,169 @@
import React, {PureComponent, ChangeEvent} from 'react'
import _ from 'lodash'
import {proxy} from 'src/utils/queryUrlGenerator'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import {parseMetaQuery, isInvalidMetaQuery} from 'src/tempVars/utils/parsing'
import {getDeep} from 'src/utils/wrappers'
import {
TemplateBuilderProps,
RemoteDataState,
TemplateValueType,
} from 'src/types'
const DEBOUNCE_DELAY = 750
interface State {
metaQueryInput: string // bound to input
metaQuery: string // debounced view of metaQueryInput
metaQueryResults: string[]
metaQueryResultsStatus: RemoteDataState
}
@ErrorHandling
class CustomMetaQueryTemplateBuilder extends PureComponent<
TemplateBuilderProps,
State
> {
private handleMetaQueryChange: () => void = _.debounce(() => {
const {metaQuery, metaQueryInput} = this.state
if (metaQuery === metaQueryInput) {
return
}
this.setState({metaQuery: metaQueryInput}, this.executeQuery)
}, DEBOUNCE_DELAY)
constructor(props: TemplateBuilderProps) {
super(props)
const metaQuery = getDeep<string>(props.template, 'query.influxql', '')
this.state = {
metaQuery,
metaQueryInput: metaQuery,
metaQueryResults: [],
metaQueryResultsStatus: RemoteDataState.NotStarted,
}
}
public componentDidMount() {
this.executeQuery()
}
public render() {
const {metaQueryInput} = this.state
return (
<div className="temp-builder csv-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<textarea
className="form-control"
value={metaQueryInput}
onChange={this.handleMetaQueryInputChange}
onBlur={this.handleMetaQueryChange}
/>
</div>
</div>
{this.renderResults()}
</div>
)
}
private renderResults() {
const {metaQueryResults, metaQueryResultsStatus} = this.state
if (this.showInvalidMetaQueryMessage) {
return (
<div className="temp-builder-results">
<p className="error">Meta Query is not valid.</p>
</div>
)
}
return (
<TemplateMetaQueryPreview
items={metaQueryResults}
loadingStatus={metaQueryResultsStatus}
/>
)
}
private get showInvalidMetaQueryMessage(): boolean {
const {metaQuery} = this.state
return this.isInvalidMetaQuery && metaQuery !== ''
}
private get isInvalidMetaQuery(): boolean {
const {metaQuery} = this.state
return isInvalidMetaQuery(metaQuery)
}
private handleMetaQueryInputChange = (
e: ChangeEvent<HTMLTextAreaElement>
) => {
this.setState({metaQueryInput: e.target.value})
this.handleMetaQueryChange()
}
private executeQuery = async (): Promise<void> => {
const {template, source, onUpdateTemplate} = this.props
const {metaQuery} = this.state
if (this.isInvalidMetaQuery) {
return
}
this.setState({metaQueryResultsStatus: RemoteDataState.Loading})
try {
const {data} = await proxy({
source: source.links.proxy,
query: metaQuery,
})
const metaQueryResults = parseMetaQuery(metaQuery, data)
this.setState({
metaQueryResults,
metaQueryResultsStatus: RemoteDataState.Done,
})
const nextValues = metaQueryResults.map(result => {
return {
type: TemplateValueType.MetaQuery,
value: result,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
const nextTemplate = {
...template,
values: nextValues,
query: {
influxql: metaQuery,
},
}
onUpdateTemplate(nextTemplate)
} catch {
this.setState({
metaQueryResults: [],
metaQueryResultsStatus: RemoteDataState.Error,
})
}
}
}
export default CustomMetaQueryTemplateBuilder

View File

@ -18,6 +18,7 @@ import MeasurementsTemplateBuilder from 'src/tempVars/components/MeasurementsTem
import FieldKeysTemplateBuilder from 'src/tempVars/components/FieldKeysTemplateBuilder'
import TagKeysTemplateBuilder from 'src/tempVars/components/TagKeysTemplateBuilder'
import TagValuesTemplateBuilder from 'src/tempVars/components/TagValuesTemplateBuilder'
import MetaQueryTemplateBuilder from 'src/tempVars/components/MetaQueryTemplateBuilder'
import {
Template,
@ -59,6 +60,7 @@ const TEMPLATE_BUILDERS = {
[TemplateType.FieldKeys]: FieldKeysTemplateBuilder,
[TemplateType.TagKeys]: TagKeysTemplateBuilder,
[TemplateType.TagValues]: TagValuesTemplateBuilder,
[TemplateType.MetaQuery]: MetaQueryTemplateBuilder,
}
const formatName = name => `:${name.replace(/:/g, '')}:`

View File

@ -34,6 +34,10 @@ export const TEMPLATE_TYPES_LIST: TemplateTypesListItem[] = [
text: 'CSV',
type: TemplateType.CSV,
},
{
text: 'Custom Meta Query',
type: TemplateType.MetaQuery,
},
]
export const TEMPLATE_VARIABLE_TYPES = {
@ -43,6 +47,7 @@ export const TEMPLATE_VARIABLE_TYPES = {
[TemplateType.FieldKeys]: TemplateValueType.FieldKey,
[TemplateType.TagKeys]: TemplateValueType.TagKey,
[TemplateType.TagValues]: TemplateValueType.TagValue,
[TemplateType.MetaQuery]: TemplateValueType.MetaQuery,
}
export const TEMPLATE_VARIABLE_QUERIES = {
@ -136,6 +141,18 @@ export const DEFAULT_TEMPLATES: DefaultTemplates = {
},
}
},
[TemplateType.MetaQuery]: () => {
return {
id: uuid.v4(),
tempVar: ':my-meta-query:',
values: [],
type: TemplateType.MetaQuery,
label: '',
query: {
influxql: '',
},
}
},
}
export const RESERVED_TEMPLATE_NAMES = [

View File

@ -0,0 +1,84 @@
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import parseShowFieldKeys from 'src/shared/parsing/showFieldKeys'
import parseShowTagKeys from 'src/shared/parsing/showTagKeys'
import parseShowTagValues from 'src/shared/parsing/showTagValues'
import parseShowMeasurements from 'src/shared/parsing/showMeasurements'
import parseShowSeries from 'src/shared/parsing/showSeries'
export const parseMetaQuery = (metaQuery: string, response): string[] => {
const metaQueryStart = getMetaQueryPrefix(metaQuery)
if (!metaQueryStart) {
throw new Error('Could not find parser for meta query')
}
const parser = PARSERS[metaQueryStart]
const extractor = EXTRACTORS[metaQueryStart]
const parsed = parser(response)
if (parsed.errors.length) {
throw new Error(parsed.errors)
}
return extractor(parsed)
}
export const isInvalidMetaQuery = (metaQuery: string): boolean =>
!getMetaQueryPrefix(metaQuery)
const getMetaQueryPrefix = (metaQuery: string): string | null => {
const words = metaQuery
.trim()
.toUpperCase()
.split(' ')
const firstTwoWords = words.slice(0, 2).join(' ')
const firstThreeWords = words.slice(0, 3).join(' ')
return VALID_META_QUERY_PREFIXES.find(
q => q === firstTwoWords || q === firstThreeWords
)
}
const VALID_META_QUERY_PREFIXES = [
'SHOW DATABASES',
'SHOW MEASUREMENTS',
'SHOW SERIES',
'SHOW TAG VALUES',
'SHOW FIELD KEYS',
'SHOW TAG KEYS',
]
const PARSERS = {
'SHOW DATABASES': parseShowDatabases,
'SHOW FIELD KEYS': parseShowFieldKeys,
'SHOW MEASUREMENTS': parseShowMeasurements,
'SHOW SERIES': parseShowSeries,
'SHOW TAG VALUES': parseShowTagValues,
'SHOW TAG KEYS': parseShowTagKeys,
}
const EXTRACTORS = {
'SHOW DATABASES': parsed => parsed.databases,
'SHOW FIELD KEYS': parsed => {
const {fieldSets} = parsed
const fieldSetsValues = Object.values(fieldSets) as string[]
return fieldSetsValues.reduce((acc, current) => [...acc, ...current], [])
},
'SHOW MEASUREMENTS': parsed => {
const {measurementSets} = parsed
return measurementSets.reduce(
(acc, current) => [...acc, ...current.measurements],
[]
)
},
'SHOW TAG KEYS': parsed => parsed.tagKeys,
'SHOW TAG VALUES': parsed => {
const {tags} = parsed
const tagsValues = Object.values(tags) as string[]
return tagsValues.reduce((acc, current) => [...acc, ...current], [])
},
'SHOW SERIES': parsed => parsed.series,
}

View File

@ -9,6 +9,7 @@ export enum TemplateValueType {
CSV = 'csv',
Points = 'points',
Constant = 'constant',
MetaQuery = 'influxql',
}
export interface TemplateValue {
@ -34,8 +35,8 @@ export enum TemplateType {
TagKeys = 'tagKeys',
TagValues = 'tagValues',
CSV = 'csv',
Query = 'query',
Databases = 'databases',
MetaQuery = 'influxql',
}
export interface Template {