Merge pull request #3734 from influxdata/tempVars/meta-queries
Add "Custom Meta Query" template variable typepull/10616/head
commit
c81658f938
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -57,7 +57,7 @@ func TestValidTemplateRequest(t *testing.T) {
|
|||
name: "No query set",
|
||||
wantErr: true,
|
||||
template: &chronograf.Template{
|
||||
Type: "query",
|
||||
Type: "influxql",
|
||||
},
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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, '')}:`
|
||||
|
|
|
@ -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 = [
|
||||
|
|
|
@ -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,
|
||||
}
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue