Merge pull request #3817 from influxdata/tempvars/map-type-upload

Tempvars/map type upload
pull/10616/head
Deniz Kusefoglu 2018-07-03 15:32:44 -07:00 committed by GitHub
commit fec39a1ca5
15 changed files with 212 additions and 26 deletions

View File

@ -97,7 +97,7 @@ message Template {
} }
message TemplateValue { message TemplateValue {
string type = 1; // Type can be tagKey, tagValue, fieldKey, csv, measurement, database, constant string type = 1; // Type can be tagKey, tagValue, fieldKey, csv, map, measurement, database, constant
string value = 2; // Value is the specific value used to replace a template in an InfluxQL query string value = 2; // Value is the specific value used to replace a template in an InfluxQL query
bool selected = 3; // Selected states that this variable has been picked to use for replacement bool selected = 3; // Selected states that this variable has been picked to use for replacement
string key = 4; // Key is the key for a specific Value if the Template Type is map (optional) string key = 4; // Key is the key for a specific Value if the Template Type is map (optional)

View File

@ -159,7 +159,7 @@ type Range struct {
// TemplateValue is a value use to replace a template in an InfluxQL query // TemplateValue is a value use to replace a template in an InfluxQL query
type TemplateValue struct { type TemplateValue struct {
Value string `json:"value"` // Value is the specific value used to replace a template in an InfluxQL query 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, influxql Type string `json:"type"` // Type can be tagKey, tagValue, fieldKey, csv, map, measurement, database, constant, influxql
Selected bool `json:"selected"` // Selected states that this variable has been picked to use for replacement 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' 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 { type Template struct {
TemplateVar TemplateVar
ID TemplateID `json:"id"` // ID is the unique ID associated with this template 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, measurements, databases, map, influxql 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 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 Query *TemplateQuery `json:"query,omitempty"` // Query is used to generate the choices for a template
} }

View File

@ -4179,7 +4179,7 @@
}, },
"type": { "type": {
"type": "string", "type": "string",
"enum": ["csv", "tagKey", "tagValue", "fieldKey", "timeStamp"], "enum": ["csv", "tagKey", "tagValue", "fieldKey", "timeStamp", "map"],
"description": "description":
"The type will change the format of the output value. tagKey/fieldKey are double quoted; tagValue are single quoted; csv and timeStamp are not quoted." "The type will change the format of the output value. tagKey/fieldKey are double quoted; tagValue are single quoted; csv and timeStamp are not quoted."
}, },

View File

@ -22,7 +22,7 @@ func ValidTemplateRequest(template *chronograf.Template) error {
switch v.Type { switch v.Type {
default: default:
return fmt.Errorf("Unknown template variable type %s", v.Type) return fmt.Errorf("Unknown template variable type %s", v.Type)
case "csv", "fieldKey", "tagKey", "tagValue", "measurement", "database", "constant", "influxql": case "csv", "map", "fieldKey", "tagKey", "tagValue", "measurement", "database", "constant", "influxql":
} }
if template.Type == "map" && v.Key == "" { if template.Type == "map" && v.Key == "" {

View File

@ -69,7 +69,7 @@ func TestValidTemplateRequest(t *testing.T) {
{ {
Key: "key", Key: "key",
Value: "value", Value: "value",
Type: "constant", Type: "map",
}, },
}, },
}, },
@ -84,7 +84,7 @@ func TestValidTemplateRequest(t *testing.T) {
Values: []chronograf.TemplateValue{ Values: []chronograf.TemplateValue{
{ {
Value: "value", Value: "value",
Type: "constant", Type: "map",
}, },
}, },
}, },

View File

@ -312,7 +312,10 @@ const removeUnselectedTemplateValues = (
'templates', 'templates',
[] []
).map(template => { ).map(template => {
if (template.type === TempVarsModels.TemplateType.CSV) { if (
template.type === TempVarsModels.TemplateType.CSV ||
template.type === TempVarsModels.TemplateType.Map
) {
return template return template
} }

View File

@ -541,6 +541,12 @@ export const notifyInvalidTimeRangeValueInURLQuery = (): Notification => ({
message: `Invalid URL query value supplied for lower or upper time range.`, message: `Invalid URL query value supplied for lower or upper time range.`,
}) })
export const notifyInvalidMapType = (): Notification => ({
...defaultErrorNotification,
icon: 'cube',
message: `Template Variables of map type accept two comma separated values per line`,
})
export const notifyInvalidZoomedTimeRangeValueInURLQuery = (): Notification => ({ export const notifyInvalidZoomedTimeRangeValueInURLQuery = (): Notification => ({
...defaultErrorNotification, ...defaultErrorNotification,
icon: 'cube', icon: 'cube',

View File

@ -0,0 +1,154 @@
import React, {PureComponent, ChangeEvent} from 'react'
import {getDeep} from 'src/utils/wrappers'
import Papa from 'papaparse'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplatePreviewList from 'src/tempVars/components/TemplatePreviewList'
import DragAndDrop from 'src/shared/components/DragAndDrop'
import {
notifyCSVUploadFailed,
notifyInvalidMapType,
} from 'src/shared/copy/notifications'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
import {trimAndRemoveQuotes} from 'src/tempVars/utils/parsing'
interface State {
templateValuesString: string
}
@ErrorHandling
class MapTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
public constructor(props: TemplateBuilderProps) {
super(props)
const templateValues = props.template.values.map(v => v.value)
const templateKeys = props.template.values.map(v => v.key)
const templateValuesString = templateKeys
.map((v, i) => `${v}, ${templateValues[i]}`)
.join('\n')
this.state = {
templateValuesString,
}
}
public render() {
const {onUpdateDefaultTemplateValue, template} = this.props
const {templateValuesString} = this.state
const pluralizer = template.values.length === 1 ? '' : 's'
return (
<>
<div className="form-group col-xs-12">
<label>Upload a CSV File</label>
<DragAndDrop
submitText="Preview"
fileTypesToAccept={this.validFileExtension}
handleSubmit={this.handleUploadFile}
submitOnDrop={true}
compact={true}
/>
</div>
<div className="form-group col-xs-12">
<label>Comma Separated Values</label>
<div className="temp-builder--mq-controls">
<textarea
className="form-control input-sm"
value={templateValuesString}
onChange={this.handleChange}
onBlur={this.handleBlur}
/>
</div>
</div>
<div className="form-group col-xs-12 temp-builder--results">
<p className="temp-builder--validation">
Mapping contains <strong>{template.values.length}</strong> key-value
pair{pluralizer}
</p>
{template.values.length > 0 && (
<TemplatePreviewList
items={template.values}
onUpdateDefaultTemplateValue={onUpdateDefaultTemplateValue}
/>
)}
</div>
</>
)
}
private handleUploadFile = (
uploadContent: string,
fileName: string
): void => {
const {template, onUpdateTemplate} = this.props
const fileExtensionRegex = new RegExp(`${this.validFileExtension}$`)
if (!fileName.match(fileExtensionRegex)) {
this.props.notify(notifyCSVUploadFailed())
return
}
this.setState({templateValuesString: uploadContent})
const nextValues = this.constructValuesFromString(uploadContent)
onUpdateTemplate({...template, values: nextValues})
}
private handleBlur = (): void => {
const {template, onUpdateTemplate} = this.props
const {templateValuesString} = this.state
const values = this.constructValuesFromString(templateValuesString)
onUpdateTemplate({...template, values})
}
private get validFileExtension(): string {
return '.csv'
}
private handleChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
this.setState({templateValuesString: e.target.value})
}
private constructValuesFromString(templateValuesString) {
const {notify} = this.props
const parsedTVS = Papa.parse(templateValuesString)
const templateValuesData = getDeep<string[][]>(parsedTVS, 'data', [[]])
if (templateValuesData.length === 0) {
return
}
let arrayOfKeys = []
let values = []
_.forEach(templateValuesData, arr => {
if (arr.length === 2 || (arr.length === 3 && arr[2] === '')) {
const key = trimAndRemoveQuotes(arr[0])
const value = trimAndRemoveQuotes(arr[1])
if (!arrayOfKeys.includes(key) && key !== '') {
values = [
...values,
{
type: TemplateValueType.Map,
value,
key,
selected: false,
localSelected: false,
},
]
arrayOfKeys = [...arrayOfKeys, key]
}
} else {
notify(notifyInvalidMapType())
}
})
return values
}
}
export default MapTemplateBuilder

View File

@ -4,9 +4,9 @@ import Dropdown from 'src/shared/components/Dropdown'
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology' import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor' import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar' import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
import Authorized, {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized' import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Template, Source} from 'src/types' import {Template, Source, TemplateValueType} from 'src/types'
interface Props { interface Props {
template: Template template: Template
@ -33,20 +33,15 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
} }
public render() { public render() {
const { const {template, source, onPickTemplate, onCreateTemplate} = this.props
template,
isUsingAuth,
meRole,
source,
onPickTemplate,
onCreateTemplate,
} = this.props
const {isEditing} = this.state const {isEditing} = this.state
const dropdownItems = template.values.map(value => ({ const dropdownItems = template.values.map(value => {
...value, if (value.type === TemplateValueType.Map) {
text: value.value, return {...value, text: value.key}
})) }
return {...value, text: value.value}
})
const dropdownStyle = template.values.length const dropdownStyle = template.values.length
? {minWidth: calculateDropdownWidth(template.values)} ? {minWidth: calculateDropdownWidth(template.values)}
@ -63,7 +58,6 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
menuClass="dropdown-astronaut" menuClass="dropdown-astronaut"
useAutoComplete={true} useAutoComplete={true}
selected={localSelectedItem.text} selected={localSelectedItem.text}
disabled={isUsingAuth && !isUserAuthorized(meRole, EDITOR_ROLE)}
onChoose={onPickTemplate(template.id)} onChoose={onPickTemplate(template.id)}
/> />
<Authorized requiredRole={EDITOR_ROLE}> <Authorized requiredRole={EDITOR_ROLE}>

View File

@ -42,8 +42,8 @@ class TemplatePreviewList extends PureComponent<Props> {
private get resultsListHeight() { private get resultsListHeight() {
const {items} = this.props const {items} = this.props
const count = Math.min(items.length, RESULTS_TO_DISPLAY) const count = Math.min(items.length, RESULTS_TO_DISPLAY)
const scrollOffset = count > RESULTS_TO_DISPLAY ? OFFSET : 0
return count * (LI_HEIGHT + LI_MARGIN_BOTTOM) - OFFSET return count * (LI_HEIGHT + LI_MARGIN_BOTTOM) - scrollOffset
} }
} }

View File

@ -2,7 +2,7 @@ import React, {PureComponent} from 'react'
import classNames from 'classnames' import classNames from 'classnames'
import {TEMPLATE_PREVIEW_LIST_DIMENSIONS as DIMENSIONS} from 'src/tempVars/constants' import {TEMPLATE_PREVIEW_LIST_DIMENSIONS as DIMENSIONS} from 'src/tempVars/constants'
import {TemplateValue} from 'src/types' import {TemplateValue, TemplateValueType} from 'src/types'
const {LI_HEIGHT, LI_MARGIN_BOTTOM} = DIMENSIONS const {LI_HEIGHT, LI_MARGIN_BOTTOM} = DIMENSIONS
@ -28,6 +28,7 @@ class TemplatePreviewListItem extends PureComponent<Props> {
active: this.isDefault, active: this.isDefault,
})} })}
> >
{this.mapTempVarKey}
{item.value} {item.value}
{this.defaultIndicator()} {this.defaultIndicator()}
</li> </li>
@ -38,6 +39,13 @@ class TemplatePreviewListItem extends PureComponent<Props> {
return this.props.item.selected return this.props.item.selected
} }
private get mapTempVarKey(): string {
const {item} = this.props
if (item.type === TemplateValueType.Map) {
return `${item.key} -->`
}
}
private defaultIndicator(): JSX.Element { private defaultIndicator(): JSX.Element {
if (this.isDefault) { if (this.isDefault) {
return <div className="temp-builder--default">DEFAULT</div> return <div className="temp-builder--default">DEFAULT</div>

View File

@ -23,6 +23,7 @@ import {
import DatabasesTemplateBuilder from 'src/tempVars/components/DatabasesTemplateBuilder' import DatabasesTemplateBuilder from 'src/tempVars/components/DatabasesTemplateBuilder'
import CSVTemplateBuilder from 'src/tempVars/components/CSVTemplateBuilder' import CSVTemplateBuilder from 'src/tempVars/components/CSVTemplateBuilder'
import MapTemplateBuilder from 'src/tempVars/components/MapTemplateBuilder'
import MeasurementsTemplateBuilder from 'src/tempVars/components/MeasurementsTemplateBuilder' import MeasurementsTemplateBuilder from 'src/tempVars/components/MeasurementsTemplateBuilder'
import FieldKeysTemplateBuilder from 'src/tempVars/components/FieldKeysTemplateBuilder' import FieldKeysTemplateBuilder from 'src/tempVars/components/FieldKeysTemplateBuilder'
import TagKeysTemplateBuilder from 'src/tempVars/components/TagKeysTemplateBuilder' import TagKeysTemplateBuilder from 'src/tempVars/components/TagKeysTemplateBuilder'
@ -66,6 +67,7 @@ interface State {
const TEMPLATE_BUILDERS = { const TEMPLATE_BUILDERS = {
[TemplateType.Databases]: DatabasesTemplateBuilder, [TemplateType.Databases]: DatabasesTemplateBuilder,
[TemplateType.CSV]: CSVTemplateBuilder, [TemplateType.CSV]: CSVTemplateBuilder,
[TemplateType.Map]: MapTemplateBuilder,
[TemplateType.Measurements]: MeasurementsTemplateBuilder, [TemplateType.Measurements]: MeasurementsTemplateBuilder,
[TemplateType.FieldKeys]: FieldKeysTemplateBuilder, [TemplateType.FieldKeys]: FieldKeysTemplateBuilder,
[TemplateType.TagKeys]: TagKeysTemplateBuilder, [TemplateType.TagKeys]: TagKeysTemplateBuilder,

View File

@ -34,6 +34,10 @@ export const TEMPLATE_TYPES_LIST: TemplateTypesListItem[] = [
text: 'CSV', text: 'CSV',
type: TemplateType.CSV, type: TemplateType.CSV,
}, },
{
text: 'Map',
type: TemplateType.Map,
},
{ {
text: 'Custom Meta Query', text: 'Custom Meta Query',
type: TemplateType.MetaQuery, type: TemplateType.MetaQuery,
@ -42,6 +46,7 @@ export const TEMPLATE_TYPES_LIST: TemplateTypesListItem[] = [
export const TEMPLATE_VARIABLE_TYPES = { export const TEMPLATE_VARIABLE_TYPES = {
[TemplateType.CSV]: TemplateValueType.CSV, [TemplateType.CSV]: TemplateValueType.CSV,
[TemplateType.Map]: TemplateValueType.Map,
[TemplateType.Databases]: TemplateValueType.Database, [TemplateType.Databases]: TemplateValueType.Database,
[TemplateType.Measurements]: TemplateValueType.Measurement, [TemplateType.Measurements]: TemplateValueType.Measurement,
[TemplateType.FieldKeys]: TemplateValueType.FieldKey, [TemplateType.FieldKeys]: TemplateValueType.FieldKey,
@ -106,6 +111,16 @@ export const DEFAULT_TEMPLATES: DefaultTemplates = {
query: {}, query: {},
} }
}, },
[TemplateType.Map]: () => {
return {
id: uuid.v4(),
tempVar: '',
values: [],
type: TemplateType.Map,
label: '',
query: {},
}
},
[TemplateType.TagKeys]: () => { [TemplateType.TagKeys]: () => {
return { return {
id: uuid.v4(), id: uuid.v4(),

View File

@ -89,6 +89,7 @@ const renderTemplate = (query: string, template: Template): string => {
case TemplateValueType.CSV: case TemplateValueType.CSV:
case TemplateValueType.Constant: case TemplateValueType.Constant:
case TemplateValueType.MetaQuery: case TemplateValueType.MetaQuery:
case TemplateValueType.Map:
return replaceAll(q, tempVar, value) return replaceAll(q, tempVar, value)
default: default:
return query return query

View File

@ -8,6 +8,7 @@ export enum TemplateValueType {
Measurement = 'measurement', Measurement = 'measurement',
TagValue = 'tagValue', TagValue = 'tagValue',
CSV = 'csv', CSV = 'csv',
Map = 'map',
Points = 'points', Points = 'points',
Constant = 'constant', Constant = 'constant',
MetaQuery = 'influxql', MetaQuery = 'influxql',
@ -19,6 +20,7 @@ export interface TemplateValue {
type: TemplateValueType type: TemplateValueType
selected: boolean selected: boolean
localSelected: boolean localSelected: boolean
key?: string
} }
export interface TemplateQuery { export interface TemplateQuery {
@ -38,6 +40,7 @@ export enum TemplateType {
TagKeys = 'tagKeys', TagKeys = 'tagKeys',
TagValues = 'tagValues', TagValues = 'tagValues',
CSV = 'csv', CSV = 'csv',
Map = 'map',
Databases = 'databases', Databases = 'databases',
MetaQuery = 'influxql', MetaQuery = 'influxql',
} }