Merge pull request #3817 from influxdata/tempvars/map-type-upload
Tempvars/map type uploadpull/10616/head
commit
fec39a1ca5
|
@ -97,7 +97,7 @@ message Template {
|
|||
}
|
||||
|
||||
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
|
||||
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)
|
||||
|
|
|
@ -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, 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
|
||||
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, 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
|
||||
Query *TemplateQuery `json:"query,omitempty"` // Query is used to generate the choices for a template
|
||||
}
|
||||
|
|
|
@ -4179,7 +4179,7 @@
|
|||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["csv", "tagKey", "tagValue", "fieldKey", "timeStamp"],
|
||||
"enum": ["csv", "tagKey", "tagValue", "fieldKey", "timeStamp", "map"],
|
||||
"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."
|
||||
},
|
||||
|
|
|
@ -22,7 +22,7 @@ func ValidTemplateRequest(template *chronograf.Template) error {
|
|||
switch v.Type {
|
||||
default:
|
||||
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 == "" {
|
||||
|
|
|
@ -69,7 +69,7 @@ func TestValidTemplateRequest(t *testing.T) {
|
|||
{
|
||||
Key: "key",
|
||||
Value: "value",
|
||||
Type: "constant",
|
||||
Type: "map",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -84,7 +84,7 @@ func TestValidTemplateRequest(t *testing.T) {
|
|||
Values: []chronograf.TemplateValue{
|
||||
{
|
||||
Value: "value",
|
||||
Type: "constant",
|
||||
Type: "map",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -312,7 +312,10 @@ const removeUnselectedTemplateValues = (
|
|||
'templates',
|
||||
[]
|
||||
).map(template => {
|
||||
if (template.type === TempVarsModels.TemplateType.CSV) {
|
||||
if (
|
||||
template.type === TempVarsModels.TemplateType.CSV ||
|
||||
template.type === TempVarsModels.TemplateType.Map
|
||||
) {
|
||||
return template
|
||||
}
|
||||
|
||||
|
|
|
@ -541,6 +541,12 @@ export const notifyInvalidTimeRangeValueInURLQuery = (): Notification => ({
|
|||
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 => ({
|
||||
...defaultErrorNotification,
|
||||
icon: 'cube',
|
||||
|
|
|
@ -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
|
|
@ -4,9 +4,9 @@ import Dropdown from 'src/shared/components/Dropdown'
|
|||
import OverlayTechnology from 'src/reusable_ui/components/overlays/OverlayTechnology'
|
||||
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
|
||||
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 {
|
||||
template: Template
|
||||
|
@ -33,20 +33,15 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {
|
||||
template,
|
||||
isUsingAuth,
|
||||
meRole,
|
||||
source,
|
||||
onPickTemplate,
|
||||
onCreateTemplate,
|
||||
} = this.props
|
||||
const {template, source, onPickTemplate, onCreateTemplate} = this.props
|
||||
const {isEditing} = this.state
|
||||
|
||||
const dropdownItems = template.values.map(value => ({
|
||||
...value,
|
||||
text: value.value,
|
||||
}))
|
||||
const dropdownItems = template.values.map(value => {
|
||||
if (value.type === TemplateValueType.Map) {
|
||||
return {...value, text: value.key}
|
||||
}
|
||||
return {...value, text: value.value}
|
||||
})
|
||||
|
||||
const dropdownStyle = template.values.length
|
||||
? {minWidth: calculateDropdownWidth(template.values)}
|
||||
|
@ -63,7 +58,6 @@ class TemplateControlDropdown extends PureComponent<Props, State> {
|
|||
menuClass="dropdown-astronaut"
|
||||
useAutoComplete={true}
|
||||
selected={localSelectedItem.text}
|
||||
disabled={isUsingAuth && !isUserAuthorized(meRole, EDITOR_ROLE)}
|
||||
onChoose={onPickTemplate(template.id)}
|
||||
/>
|
||||
<Authorized requiredRole={EDITOR_ROLE}>
|
||||
|
|
|
@ -42,8 +42,8 @@ class TemplatePreviewList extends PureComponent<Props> {
|
|||
private get resultsListHeight() {
|
||||
const {items} = this.props
|
||||
const count = Math.min(items.length, RESULTS_TO_DISPLAY)
|
||||
|
||||
return count * (LI_HEIGHT + LI_MARGIN_BOTTOM) - OFFSET
|
||||
const scrollOffset = count > RESULTS_TO_DISPLAY ? OFFSET : 0
|
||||
return count * (LI_HEIGHT + LI_MARGIN_BOTTOM) - scrollOffset
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, {PureComponent} from 'react'
|
|||
import classNames from 'classnames'
|
||||
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
|
||||
|
||||
|
@ -28,6 +28,7 @@ class TemplatePreviewListItem extends PureComponent<Props> {
|
|||
active: this.isDefault,
|
||||
})}
|
||||
>
|
||||
{this.mapTempVarKey}
|
||||
{item.value}
|
||||
{this.defaultIndicator()}
|
||||
</li>
|
||||
|
@ -38,6 +39,13 @@ class TemplatePreviewListItem extends PureComponent<Props> {
|
|||
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 {
|
||||
if (this.isDefault) {
|
||||
return <div className="temp-builder--default">DEFAULT</div>
|
||||
|
|
|
@ -23,6 +23,7 @@ import {
|
|||
|
||||
import DatabasesTemplateBuilder from 'src/tempVars/components/DatabasesTemplateBuilder'
|
||||
import CSVTemplateBuilder from 'src/tempVars/components/CSVTemplateBuilder'
|
||||
import MapTemplateBuilder from 'src/tempVars/components/MapTemplateBuilder'
|
||||
import MeasurementsTemplateBuilder from 'src/tempVars/components/MeasurementsTemplateBuilder'
|
||||
import FieldKeysTemplateBuilder from 'src/tempVars/components/FieldKeysTemplateBuilder'
|
||||
import TagKeysTemplateBuilder from 'src/tempVars/components/TagKeysTemplateBuilder'
|
||||
|
@ -66,6 +67,7 @@ interface State {
|
|||
const TEMPLATE_BUILDERS = {
|
||||
[TemplateType.Databases]: DatabasesTemplateBuilder,
|
||||
[TemplateType.CSV]: CSVTemplateBuilder,
|
||||
[TemplateType.Map]: MapTemplateBuilder,
|
||||
[TemplateType.Measurements]: MeasurementsTemplateBuilder,
|
||||
[TemplateType.FieldKeys]: FieldKeysTemplateBuilder,
|
||||
[TemplateType.TagKeys]: TagKeysTemplateBuilder,
|
||||
|
|
|
@ -34,6 +34,10 @@ export const TEMPLATE_TYPES_LIST: TemplateTypesListItem[] = [
|
|||
text: 'CSV',
|
||||
type: TemplateType.CSV,
|
||||
},
|
||||
{
|
||||
text: 'Map',
|
||||
type: TemplateType.Map,
|
||||
},
|
||||
{
|
||||
text: 'Custom Meta Query',
|
||||
type: TemplateType.MetaQuery,
|
||||
|
@ -42,6 +46,7 @@ export const TEMPLATE_TYPES_LIST: TemplateTypesListItem[] = [
|
|||
|
||||
export const TEMPLATE_VARIABLE_TYPES = {
|
||||
[TemplateType.CSV]: TemplateValueType.CSV,
|
||||
[TemplateType.Map]: TemplateValueType.Map,
|
||||
[TemplateType.Databases]: TemplateValueType.Database,
|
||||
[TemplateType.Measurements]: TemplateValueType.Measurement,
|
||||
[TemplateType.FieldKeys]: TemplateValueType.FieldKey,
|
||||
|
@ -106,6 +111,16 @@ export const DEFAULT_TEMPLATES: DefaultTemplates = {
|
|||
query: {},
|
||||
}
|
||||
},
|
||||
[TemplateType.Map]: () => {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
tempVar: '',
|
||||
values: [],
|
||||
type: TemplateType.Map,
|
||||
label: '',
|
||||
query: {},
|
||||
}
|
||||
},
|
||||
[TemplateType.TagKeys]: () => {
|
||||
return {
|
||||
id: uuid.v4(),
|
||||
|
|
|
@ -89,6 +89,7 @@ const renderTemplate = (query: string, template: Template): string => {
|
|||
case TemplateValueType.CSV:
|
||||
case TemplateValueType.Constant:
|
||||
case TemplateValueType.MetaQuery:
|
||||
case TemplateValueType.Map:
|
||||
return replaceAll(q, tempVar, value)
|
||||
default:
|
||||
return query
|
||||
|
|
|
@ -8,6 +8,7 @@ export enum TemplateValueType {
|
|||
Measurement = 'measurement',
|
||||
TagValue = 'tagValue',
|
||||
CSV = 'csv',
|
||||
Map = 'map',
|
||||
Points = 'points',
|
||||
Constant = 'constant',
|
||||
MetaQuery = 'influxql',
|
||||
|
@ -19,6 +20,7 @@ export interface TemplateValue {
|
|||
type: TemplateValueType
|
||||
selected: boolean
|
||||
localSelected: boolean
|
||||
key?: string
|
||||
}
|
||||
|
||||
export interface TemplateQuery {
|
||||
|
@ -38,6 +40,7 @@ export enum TemplateType {
|
|||
TagKeys = 'tagKeys',
|
||||
TagValues = 'tagValues',
|
||||
CSV = 'csv',
|
||||
Map = 'map',
|
||||
Databases = 'databases',
|
||||
MetaQuery = 'influxql',
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue