Introduce new template variables UI

PR: #3683
pull/10616/head
Christopher Henn 2018-06-12 13:20:30 -07:00
parent 3836444bea
commit e6cd29da58
45 changed files with 2151 additions and 1302 deletions

View File

@ -61,6 +61,7 @@ import {
Cell,
Source,
Template,
TemplateType,
URLQueryParams,
} from 'src/types'
import {CellType, DashboardName} from 'src/types/dashboard'
@ -444,7 +445,7 @@ export const getChronografVersion = () => async (): Promise<string | void> => {
const removeUnselectedTemplateValues = (dashboard: Dashboard): Template[] => {
const templates = getDeep<Template[]>(dashboard, 'templates', []).map(
template => {
if (template.type === 'csv') {
if (template.type === TemplateType.CSV) {
return template
}

View File

@ -19,10 +19,8 @@ import {getQueryConfigAndStatus} from 'src/shared/apis'
import {IS_STATIC_LEGEND} from 'src/shared/constants'
import {nextSource} from 'src/dashboards/utils/sources'
import {
removeUnselectedTemplateValues,
TYPE_QUERY_CONFIG,
} from 'src/dashboards/constants'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {removeUnselectedTemplateValues} from 'src/tempVars/constants'
import {OVERLAY_TECHNOLOGY} from 'src/shared/constants/classNames'
import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants'
import {

View File

@ -6,7 +6,8 @@ import QueryTabList from 'src/shared/components/QueryTabList'
import QueryTextArea from 'src/dashboards/components/QueryTextArea'
import SchemaExplorer from 'src/shared/components/SchemaExplorer'
import {buildQuery} from 'src/utils/influxql'
import {TYPE_QUERY_CONFIG, TEMPLATE_RANGE} from 'src/dashboards/constants'
import {TYPE_QUERY_CONFIG} from 'src/dashboards/constants'
import {TEMPLATE_RANGE} from 'src/tempVars/constants'
import {QueryConfig, Source, SourceLinks, TimeRange} from 'src/types'
import {CellEditorOverlayActions} from 'src/dashboards/components/CellEditorOverlay'

View File

@ -1,71 +0,0 @@
import React, {Component} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import uuid from 'uuid'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import TemplateControlDropdown from 'src/dashboards/components/TemplateControlDropdown'
import {Template} from 'src/types/tempVars'
interface Props {
meRole: string
isUsingAuth: boolean
templates: Template[]
isOpen: boolean
onOpenTemplateManager: () => void
onSelectTemplate: (id: string) => void
}
class TemplateControlBar extends Component<Props> {
public shouldComponentUpdate(nextProps) {
return !_.isEqual(this.props, nextProps)
}
public render() {
const {
isOpen,
templates,
onSelectTemplate,
onOpenTemplateManager,
meRole,
isUsingAuth,
} = this.props
return (
<div className={classnames('template-control-bar', {show: isOpen})}>
<div className="template-control--container">
<div className="template-control--controls">
{templates && templates.length ? (
templates.map(template => (
<TemplateControlDropdown
key={uuid.v4()}
meRole={meRole}
isUsingAuth={isUsingAuth}
template={template}
onSelectTemplate={onSelectTemplate}
/>
))
) : (
<div className="template-control--empty" data-test="empty-state">
This dashboard does not have any{' '}
<strong>Template Variables</strong>
</div>
)}
</div>
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="btn btn-primary btn-sm template-control--manage"
onClick={onOpenTemplateManager}
>
<span className="icon cog-thick" />
Manage
</button>
</Authorized>
</div>
</div>
)
}
}
export default TemplateControlBar

View File

@ -1,53 +0,0 @@
import React, {SFC} from 'react'
import Dropdown from 'src/shared/components/Dropdown'
import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import {Template} from 'src/types/tempVars'
interface Props {
template: Template
meRole: string
isUsingAuth: boolean
onSelectTemplate: (id: string) => void
}
// TODO: change Dropdown to a MultiSelectDropdown, `selected` to
// the full array, and [item] to all `selected` values when we update
// this component to support multiple values
const TemplateControlDropdown: SFC<Props> = ({
template,
onSelectTemplate,
isUsingAuth,
meRole,
}) => {
const dropdownItems = template.values.map(value => ({
...value,
text: value.value,
}))
const dropdownStyle = template.values.length
? {minWidth: calculateDropdownWidth(template.values)}
: null
const selectedItem = dropdownItems.find(item => item.selected) ||
dropdownItems[0] || {text: '(No values)'}
return (
<div className="template-control--dropdown" style={dropdownStyle}>
<Dropdown
items={dropdownItems}
buttonSize="btn-xs"
menuClass="dropdown-astronaut"
useAutoComplete={true}
selected={selectedItem.text}
disabled={isUsingAuth && !isUserAuthorized(meRole, EDITOR_ROLE)}
onChoose={onSelectTemplate(template.id)}
/>
<label className="template-control--label">{template.tempVar}</label>
</div>
)
}
export default TemplateControlDropdown

View File

@ -1,383 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import uniq from 'lodash/uniq'
import OnClickOutside from 'react-onclickoutside'
import classnames from 'classnames'
import Dropdown from 'shared/components/Dropdown'
import TemplateQueryBuilder from 'src/dashboards/components/template_variables/TemplateQueryBuilder'
import TableInput from 'src/dashboards/components/template_variables/TableInput'
import RowValues from 'src/dashboards/components/template_variables/RowValues'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getTempVarValuesBySourceQuery as getTempVarValuesBySourceQueryAJAX} from 'src/dashboards/apis'
import parsers from 'shared/parsing'
import {TEMPLATE_TYPES} from 'src/dashboards/constants'
import generateTemplateVariableQuery from 'src/dashboards/utils/tempVars'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import {notify as notifyAction} from 'shared/actions/notifications'
import {notifyTempVarAlreadyExists} from 'shared/copy/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
const compact = values => uniq(values).filter(value => /\S/.test(value))
const TemplateVariableRow = ({
template: {id, tempVar, values},
isEditing,
selectedType,
selectedDatabase,
selectedMeasurement,
onSelectType,
onSelectDatabase,
onSelectMeasurement,
selectedTagKey,
onSelectTagKey,
onStartEdit,
onCancelEdit,
autoFocusTarget,
onSubmit,
onErrorThrown,
onDeleteTempVar,
source,
}) => (
<form
className={classnames('template-variable-manager--table-row', {
editing: isEditing,
})}
onSubmit={onSubmit({
selectedType,
selectedDatabase,
selectedMeasurement,
selectedTagKey,
})}
>
<div className="tvm--col-1">
<TableInput
name="tempVar"
defaultValue={tempVar}
isEditing={isEditing}
onStartEdit={onStartEdit}
autoFocusTarget={autoFocusTarget}
/>
</div>
<div className="tvm--col-2">
<Dropdown
items={TEMPLATE_TYPES}
onChoose={onSelectType}
onClick={onStartEdit('tempVar')}
selected={TEMPLATE_TYPES.find(t => t.type === selectedType).text}
className="dropdown-140"
/>
</div>
<div className="tvm--col-3">
<TemplateQueryBuilder
source={source}
onSelectDatabase={onSelectDatabase}
selectedType={selectedType}
selectedDatabase={selectedDatabase}
onSelectMeasurement={onSelectMeasurement}
selectedMeasurement={selectedMeasurement}
selectedTagKey={selectedTagKey}
onSelectTagKey={onSelectTagKey}
onStartEdit={onStartEdit('tempVar')}
onErrorThrown={onErrorThrown}
/>
<RowValues
selectedType={selectedType}
values={values}
isEditing={isEditing}
onStartEdit={onStartEdit}
autoFocusTarget={autoFocusTarget}
/>
</div>
<div className={`tvm--col-4${isEditing ? ' editing' : ''}`}>
{isEditing ? (
<div className="tvm-actions">
<button
className="btn btn-sm btn-info btn-square"
type="button"
onClick={onCancelEdit}
>
<span className="icon remove" />
</button>
<button className="btn btn-sm btn-success btn-square" type="submit">
<span className="icon checkmark" />
</button>
</div>
) : (
<div className="tvm-actions">
<ConfirmButton
type="btn-danger"
confirmText="Delete template variable?"
confirmAction={onDeleteTempVar(id)}
icon="trash"
square={true}
/>
</div>
)}
</div>
</form>
)
@ErrorHandling
class RowWrapper extends Component {
constructor(props) {
super(props)
const {
template: {type, query, isNew},
} = this.props
this.state = {
isEditing: !!isNew,
isNew: !!isNew,
hasBeenSavedToComponentStateOnce: !isNew,
selectedType: type,
selectedDatabase: query && query.db,
selectedMeasurement: query && query.measurement,
selectedTagKey: query && query.tagKey,
autoFocusTarget: 'tempVar',
}
}
handleSubmit = ({
selectedDatabase: database,
selectedMeasurement: measurement,
selectedTagKey: tagKey,
selectedType: type,
}) => async e => {
e.preventDefault()
const {
source,
template,
template: {id},
onRunQuerySuccess,
onRunQueryFailure,
tempVarAlreadyExists,
notify,
} = this.props
const _tempVar = e.target.tempVar.value.replace(/\u003a/g, '')
const tempVar = `\u003a${_tempVar}\u003a` // add ':'s
if (tempVarAlreadyExists(tempVar, id)) {
return notify(notifyTempVarAlreadyExists(_tempVar))
}
this.setState({
isEditing: false,
hasBeenSavedToComponentStateOnce: true,
})
const {query, tempVars} = generateTemplateVariableQuery({
type,
tempVar,
query: {
database,
// rp, TODO
measurement,
tagKey,
},
})
const queryConfig = {
type,
tempVars,
query,
database,
// rp: TODO
measurement,
tagKey,
}
try {
let parsedData
if (type === 'csv') {
parsedData = e.target.values.value.split(',').map(value => value.trim())
} else {
parsedData = await this.getTempVarValuesBySourceQuery(
source,
queryConfig
)
}
onRunQuerySuccess(template, queryConfig, compact(parsedData), tempVar)
} catch (error) {
onRunQueryFailure(error)
}
}
handleClickOutside() {
this.handleCancelEdit()
}
handleStartEdit = name => () => {
this.setState({isEditing: true, autoFocusTarget: name})
}
handleCancelEdit = () => {
const {
template: {
type,
query: {db, measurement, tagKey},
id,
},
onDelete,
} = this.props
const {hasBeenSavedToComponentStateOnce} = this.state
if (!hasBeenSavedToComponentStateOnce) {
return onDelete(id)
}
this.setState({
selectedType: type,
selectedDatabase: db,
selectedMeasurement: measurement,
selectedTagKey: tagKey,
isEditing: false,
})
}
handleSelectType = item => {
this.setState({
selectedType: item.type,
selectedDatabase: null,
selectedMeasurement: null,
selectedTagKey: null,
})
}
handleSelectDatabase = item => {
this.setState({selectedDatabase: item.text})
}
handleSelectMeasurement = item => {
this.setState({selectedMeasurement: item.text})
}
handleSelectTagKey = item => {
this.setState({selectedTagKey: item.text})
}
getTempVarValuesBySourceQuery = async (
source,
{query, database, rp, tempVars, type, measurement, tagKey}
) => {
try {
const {data} = await getTempVarValuesBySourceQueryAJAX(source, {
query,
db: database,
rp,
tempVars,
})
const parsedData = parsers[type](data, tagKey || measurement) // tagKey covers tagKey and fieldKey
if (parsedData.errors.length) {
throw parsedData.errors
}
return parsedData[type]
} catch (error) {
console.error(error)
throw error
}
}
handleDelete = id => () => {
this.props.onDelete(id)
}
render() {
const {
isEditing,
selectedType,
selectedDatabase,
selectedMeasurement,
selectedTagKey,
autoFocusTarget,
} = this.state
return (
<TemplateVariableRow
{...this.props}
isEditing={isEditing}
selectedType={selectedType}
selectedDatabase={selectedDatabase}
selectedMeasurement={selectedMeasurement}
selectedTagKey={selectedTagKey}
onSelectType={this.handleSelectType}
onSelectDatabase={this.handleSelectDatabase}
onSelectMeasurement={this.handleSelectMeasurement}
onSelectTagKey={this.handleSelectTagKey}
onStartEdit={this.handleStartEdit}
onCancelEdit={this.handleCancelEdit}
autoFocusTarget={autoFocusTarget}
onSubmit={this.handleSubmit}
onDeleteTempVar={this.handleDelete}
/>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
RowWrapper.propTypes = {
source: shape({
links: shape({
proxy: string,
}),
}).isRequired,
template: shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
influxql: string,
measurement: string,
tagKey: string,
}),
values: arrayOf(
shape({
value: string.isRequired,
type: string.isRequired,
selected: bool.isRequired,
})
).isRequired,
links: shape({
self: string.isRequired,
}),
}),
onRunQuerySuccess: func.isRequired,
onRunQueryFailure: func.isRequired,
onDelete: func.isRequired,
tempVarAlreadyExists: func.isRequired,
notify: func.isRequired,
}
TemplateVariableRow.propTypes = {
...RowWrapper.propTypes,
selectedType: string.isRequired,
selectedDatabase: string,
selectedTagKey: string,
onSelectType: func.isRequired,
onSelectDatabase: func.isRequired,
onSelectTagKey: func.isRequired,
onStartEdit: func.isRequired,
onCancelEdit: func.isRequired,
onSubmit: func.isRequired,
onErrorThrown: func.isRequired,
}
const mapDispatchToProps = dispatch => ({
onErrorThrown: bindActionCreators(errorThrownAction, dispatch),
notify: bindActionCreators(notifyAction, dispatch),
})
export default connect(null, mapDispatchToProps)(OnClickOutside(RowWrapper))

View File

@ -1,44 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import TableInput from 'src/dashboards/components/template_variables/TableInput'
const RowValues = ({
selectedType,
values = [],
isEditing,
onStartEdit,
autoFocusTarget,
}) => {
const _values = values.map(v => v.value).join(', ')
if (selectedType === 'csv') {
return (
<TableInput
name="values"
defaultValue={_values}
isEditing={isEditing}
onStartEdit={onStartEdit}
autoFocusTarget={autoFocusTarget}
spellCheck={false}
autoComplete="false"
/>
)
}
return (
<div className={values.length ? 'tvm-values' : 'tvm-values-empty'}>
{values.length ? _values : 'No values to display'}
</div>
)
}
const {arrayOf, bool, func, shape, string} = PropTypes
RowValues.propTypes = {
selectedType: string.isRequired,
values: arrayOf(shape()),
isEditing: bool.isRequired,
onStartEdit: func.isRequired,
autoFocusTarget: string,
}
export default RowValues

View File

@ -1,80 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import TemplateVariableRow from 'src/dashboards/components/template_variables/Row'
const TemplateVariableTable = ({
source,
templates,
onRunQuerySuccess,
onRunQueryFailure,
onDelete,
tempVarAlreadyExists,
}) => (
<div className="template-variable-manager--table">
{templates.length ? (
<div className="template-variable-manager--table-container">
<div className="template-variable-manager--table-heading">
<div className="tvm--col-1">Variable</div>
<div className="tvm--col-2">Type</div>
<div className="tvm--col-3">Definition / Values</div>
<div className="tvm--col-4" />
</div>
<div className="template-variable-manager--table-rows">
{templates.map(t => (
<TemplateVariableRow
key={t.id}
source={source}
template={t}
onRunQuerySuccess={onRunQuerySuccess}
onRunQueryFailure={onRunQueryFailure}
onDelete={onDelete}
tempVarAlreadyExists={tempVarAlreadyExists}
/>
))}
</div>
</div>
) : (
<div className="generic-empty-state">
<h4 style={{margin: '60px 0'}} className="no-user-select">
You have no Template Variables, why not create one?
</h4>
</div>
)}
</div>
)
const {arrayOf, bool, func, shape, string} = PropTypes
TemplateVariableTable.propTypes = {
source: shape({
links: shape({
proxy: string,
}),
}).isRequired,
templates: arrayOf(
shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
influxql: string,
measurement: string,
tagKey: string,
}),
values: arrayOf(
shape({
value: string.isRequired,
type: string.isRequired,
selected: bool.isRequired,
})
).isRequired,
})
),
onRunQuerySuccess: func.isRequired,
onRunQueryFailure: func.isRequired,
onDelete: func.isRequired,
tempVarAlreadyExists: func.isRequired,
}
export default TemplateVariableTable

View File

@ -1,43 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
const TableInput = ({
name,
defaultValue,
isEditing,
onStartEdit,
autoFocusTarget,
}) => {
return isEditing ? (
<div name={name} style={{width: '100%'}}>
<input
required={true}
name={name}
autoFocus={name === autoFocusTarget}
className="form-control input-sm tvm-input-edit"
type="text"
defaultValue={
name === 'tempVar'
? defaultValue.replace(/\u003a/g, '') // remove ':'s
: defaultValue
}
/>
</div>
) : (
<div style={{width: '100%'}} onClick={onStartEdit(name)}>
<div className="tvm-input">{defaultValue}</div>
</div>
)
}
const {bool, func, string} = PropTypes
TableInput.propTypes = {
defaultValue: string,
isEditing: bool.isRequired,
onStartEdit: func.isRequired,
name: string.isRequired,
autoFocusTarget: string,
}
export default TableInput

View File

@ -1,134 +0,0 @@
import React from 'react'
import PropTypes from 'prop-types'
import DatabaseDropdown from 'shared/components/DatabaseDropdown'
import MeasurementDropdown from 'src/dashboards/components/MeasurementDropdown'
import TagKeyDropdown from 'src/dashboards/components/TagKeyDropdown'
const TemplateQueryBuilder = ({
selectedType,
selectedDatabase,
selectedMeasurement,
selectedTagKey,
onSelectDatabase,
onSelectMeasurement,
onSelectTagKey,
onStartEdit,
onErrorThrown,
source,
}) => {
switch (selectedType) {
case 'csv':
return null
case 'databases':
return <div className="tvm-query-builder--text">SHOW DATABASES</div>
case 'measurements':
return (
<div className="tvm-query-builder">
<span className="tvm-query-builder--text">SHOW MEASUREMENTS ON</span>
<DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
</div>
)
case 'fieldKeys':
case 'tagKeys':
return (
<div className="tvm-query-builder">
<span className="tvm-query-builder--text">
SHOW {selectedType === 'fieldKeys' ? 'FIELD' : 'TAG'} KEYS ON
</span>
<DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
<span className="tvm-query-builder--text">FROM</span>
{selectedDatabase ? (
<MeasurementDropdown
source={source}
database={selectedDatabase}
measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
) : (
<div>No database selected</div>
)}
</div>
)
case 'tagValues':
return (
<div className="tvm-query-builder">
<span className="tvm-query-builder--text">SHOW TAG VALUES ON</span>
<DatabaseDropdown
source={source}
onSelectDatabase={onSelectDatabase}
database={selectedDatabase}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
<span className="tvm-query-builder--text">FROM</span>
{selectedDatabase ? (
<MeasurementDropdown
source={source}
database={selectedDatabase}
measurement={selectedMeasurement}
onSelectMeasurement={onSelectMeasurement}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
) : (
'Pick a DB'
)}
<span className="tvm-query-builder--text">WITH KEY =</span>
{selectedMeasurement ? (
<TagKeyDropdown
source={source}
database={selectedDatabase}
measurement={selectedMeasurement}
tagKey={selectedTagKey}
onSelectTagKey={onSelectTagKey}
onStartEdit={onStartEdit}
onErrorThrown={onErrorThrown}
/>
) : (
'Pick a Tag Key'
)}
</div>
)
default:
return (
<div>
<span className="tvm-query-builder--text">n/a</span>
</div>
)
}
}
const {func, shape, string} = PropTypes
TemplateQueryBuilder.propTypes = {
selectedType: string.isRequired,
onSelectDatabase: func.isRequired,
onSelectMeasurement: func.isRequired,
onSelectTagKey: func.isRequired,
onStartEdit: func.isRequired,
selectedMeasurement: string,
selectedDatabase: string,
selectedTagKey: string,
onErrorThrown: func.isRequired,
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
export default TemplateQueryBuilder

View File

@ -1,242 +0,0 @@
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import uuid from 'uuid'
import TemplateVariableTable from 'src/dashboards/components/template_variables/Table'
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
const TemplateVariableManager = ({
source,
onClose,
onDelete,
isEdited,
templates,
onAddVariable,
onRunQuerySuccess,
onRunQueryFailure,
tempVarAlreadyExists,
onSaveTemplatesSuccess,
onEditTemplateVariables,
}) => (
<div className="template-variable-manager">
<div className="template-variable-manager--header">
<div className="page-header__left">
<h1 className="page-header__title">Template Variables</h1>
</div>
<div className="page-header__right">
<button
className="btn btn-primary btn-sm"
type="button"
onClick={onAddVariable}
>
Add Variable
</button>
<button
className={classnames('btn btn-success btn-sm', {
disabled: !isEdited,
})}
type="button"
onClick={onEditTemplateVariables(templates, onSaveTemplatesSuccess)}
>
Save Changes
</button>
<span className="page-header__dismiss" onClick={onClose} />
</div>
</div>
<div className="template-variable-manager--body">
<TemplateVariableTable
source={source}
onDelete={onDelete}
templates={templates}
onRunQuerySuccess={onRunQuerySuccess}
onRunQueryFailure={onRunQueryFailure}
tempVarAlreadyExists={tempVarAlreadyExists}
/>
</div>
</div>
)
class TemplateVariableManagerWrapper extends Component {
constructor(props) {
super(props)
this.state = {
rows: this.props.templates,
isEdited: false,
}
}
onAddVariable = () => {
const {rows} = this.state
const newRow = {
tempVar: '',
values: [],
id: uuid.v4(),
type: 'csv',
query: {
influxql: '',
db: '',
// rp, TODO
measurement: '',
tagKey: '',
},
isNew: true,
}
const newRows = [newRow, ...rows]
this.setState({rows: newRows})
}
onRunQuerySuccess = (template, queryConfig, parsedData, tempVar) => {
const {rows} = this.state
const {id, links} = template
const {
type,
query: influxql,
database: db,
measurement,
tagKey,
} = queryConfig
// Determine which is the selectedValue, if any
const currentRow = rows.find(row => row.id === id)
let selectedValue
if (currentRow && currentRow.values && currentRow.values.length) {
const matchedValue = currentRow.values.find(val => val.selected)
if (matchedValue) {
selectedValue = matchedValue.value
}
}
if (
!selectedValue &&
currentRow &&
currentRow.values &&
currentRow.values.length
) {
selectedValue = currentRow.values[0].value
}
if (!selectedValue) {
selectedValue = parsedData[0]
}
const values = parsedData.map(value => ({
value,
type: TEMPLATE_VARIABLE_TYPES[type],
selected: selectedValue === value,
}))
const templateVariable = {
tempVar,
values,
id,
type,
query: {
influxql,
db,
// rp, TODO
measurement,
tagKey,
},
links,
}
const newRows = rows.map(r => (r.id === template.id ? templateVariable : r))
this.setState({rows: newRows, isEdited: true})
}
onSaveTemplatesSuccess = () => {
const {rows} = this.state
const newRows = rows.map(row => ({...row, isNew: false}))
this.setState({rows: newRows, isEdited: false})
}
onDeleteTemplateVariable = templateID => {
const {rows} = this.state
const newRows = rows.filter(({id}) => id !== templateID)
this.setState({rows: newRows, isEdited: true})
}
tempVarAlreadyExists = (testTempVar, testID) => {
const {rows: tempVars} = this.state
return tempVars.some(
({tempVar, id}) => tempVar === testTempVar && id !== testID
)
}
handleDismissManager = () => {
const {onDismissOverlay} = this.props
const {isEdited} = this.state
if (
!isEdited ||
(isEdited && confirm('Do you want to close without saving?')) // eslint-disable-line no-alert
) {
onDismissOverlay()
}
}
render() {
const {rows, isEdited} = this.state
return (
<TemplateVariableManager
{...this.props}
templates={rows}
isEdited={isEdited}
onClose={this.handleDismissManager}
onRunQuerySuccess={this.onRunQuerySuccess}
onSaveTemplatesSuccess={this.onSaveTemplatesSuccess}
onAddVariable={this.onAddVariable}
onDelete={this.onDeleteTemplateVariable}
tempVarAlreadyExists={this.tempVarAlreadyExists}
/>
)
}
}
const {arrayOf, bool, func, shape, string} = PropTypes
TemplateVariableManager.propTypes = {
...TemplateVariableManagerWrapper.propTypes,
onRunQuerySuccess: func.isRequired,
onSaveTemplatesSuccess: func.isRequired,
onAddVariable: func.isRequired,
isEdited: bool.isRequired,
onDelete: func.isRequired,
}
TemplateVariableManagerWrapper.propTypes = {
onEditTemplateVariables: func.isRequired,
templates: arrayOf(
shape({
type: string.isRequired,
tempVar: string.isRequired,
query: shape({
db: string,
influxql: string,
}),
values: arrayOf(
shape({
value: string.isRequired,
type: string.isRequired,
selected: bool.isRequired,
})
).isRequired,
})
),
onRunQueryFailure: func.isRequired,
onDismissOverlay: func,
}
export default TemplateVariableManagerWrapper

View File

@ -4,8 +4,6 @@ import {
} from 'src/shared/constants/tableGraph'
import {Cell, QueryConfig} from 'src/types'
import {CellType, Dashboard, DecimalPlaces} from 'src/types/dashboard'
import {TimeRange} from 'src/types/query'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
export const UNTITLED_GRAPH: string = 'Untitled Graph'
@ -106,84 +104,7 @@ export const NEW_DASHBOARD: NewDefaultDashboard = {
cells: [NEW_DEFAULT_DASHBOARD_CELL],
}
export const TEMPLATE_TYPES = [
{
text: 'CSV',
type: 'csv',
},
{
text: 'Databases',
type: 'databases',
},
{
text: 'Measurements',
type: 'measurements',
},
{
text: 'Field Keys',
type: 'fieldKeys',
},
{
text: 'Tag Keys',
type: 'tagKeys',
},
{
text: 'Tag Values',
type: 'tagValues',
},
]
export const TEMPLATE_VARIABLE_TYPES = {
csv: 'csv',
databases: 'database',
measurements: 'measurement',
fieldKeys: 'fieldKey',
tagKeys: 'tagKey',
tagValues: 'tagValue',
}
interface TemplateVariableQueries {
databases: string
measurements: string
fieldKeys: string
tagKeys: string
tagValues: string
}
export const TEMPLATE_VARIABLE_QUERIES: TemplateVariableQueries = {
databases: 'SHOW DATABASES',
measurements: 'SHOW MEASUREMENTS ON :database:',
fieldKeys: 'SHOW FIELD KEYS ON :database: FROM :measurement:',
tagKeys: 'SHOW TAG KEYS ON :database: FROM :measurement:',
tagValues:
'SHOW TAG VALUES ON :database: FROM :measurement: WITH KEY=:tagKey:',
}
export const MATCH_INCOMPLETE_TEMPLATES = /:[\w-]*/g
export const applyMasks = query => {
const matchWholeTemplates = /:([\w-]*):/g
const maskForWholeTemplates = '😸$1😸'
return query.replace(matchWholeTemplates, maskForWholeTemplates)
}
export const insertTempVar = (query, tempVar) => {
return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar)
}
export const unMask = query => {
return query.replace(/😸/g, ':')
}
export const removeUnselectedTemplateValues = templates => {
return templates.map(template => {
const selectedValues = template.values.filter(value => value.selected)
return {...template, values: selectedValues}
})
}
export const TYPE_QUERY_CONFIG: string = 'queryConfig'
export const TYPE_SHIFTED: string = 'shifted queryConfig'
export const TYPE_FLUX: string = 'flux'
export const DASHBOARD_NAME_MAX_LENGTH: number = 50
export const TEMPLATE_RANGE: TimeRange = {
upper: null,
lower: TEMP_VAR_DASHBOARD_TIME,
}

View File

@ -11,9 +11,8 @@ import {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import CellEditorOverlay from 'src/dashboards/components/CellEditorOverlay'
import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import Dashboard from 'src/dashboards/components/Dashboard'
import TemplateVariableManager from 'src/dashboards/components/template_variables/TemplateVariableManager'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import TemplateControlBar from 'src/dashboards/components/TemplateControlBar'
import TemplateControlBar from 'src/tempVars/components/TemplateControlBar'
import {errorThrown as errorThrownAction} from 'shared/actions/errors'
import {notify as notifyAction} from 'shared/actions/notifications'
@ -48,7 +47,6 @@ import {FORMAT_INFLUXQL, defaultTimeRange} from 'src/shared/data/timeRanges'
import {colorsStringSchema, colorsNumberSchema} from 'shared/schemas'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OverlayContext} from 'src/shared/components/OverlayTechnology'
import {getDeep} from 'src/utils/wrappers'
@ -174,31 +172,6 @@ class DashboardPage extends Component {
return topInView && bottomInView
}
handleOpenTemplateManager = () => {
const {handleShowOverlay, dashboard, source} = this.props
const options = {
dismissOnClickOutside: false,
dismissOnEscape: false,
}
handleShowOverlay(
<OverlayContext.Consumer>
{({onDismissOverlay}) => {
return (
<TemplateVariableManager
source={source}
templates={dashboard.templates}
onDismissOverlay={onDismissOverlay}
onRunQueryFailure={this.handleRunQueryFailure}
onEditTemplateVariables={this.handleEditTemplateVariables}
/>
)
}}
</OverlayContext.Consumer>,
options
)
}
handleSaveEditedCell = newCell => {
const {
dashboardActions,
@ -311,10 +284,7 @@ class DashboardPage extends Component {
dashboardActions.putDashboardByID(dashboardID)
}
handleEditTemplateVariables = (
templates,
onSaveTemplatesSuccess
) => async () => {
handleSaveTemplateVariables = async templates => {
const {location, dashboardActions, dashboard} = this.props
try {
@ -322,7 +292,6 @@ class DashboardPage extends Component {
...dashboard,
templates,
})
onSaveTemplatesSuccess()
const deletedTempVars = dashboard.templates.filter(
({tempVar: oldTempVar}) =>
!templates.find(({tempVar: newTempVar}) => oldTempVar === newTempVar)
@ -481,9 +450,10 @@ class DashboardPage extends Component {
templates={dashboard && dashboard.templates}
meRole={meRole}
isUsingAuth={isUsingAuth}
onSaveTemplates={this.handleSaveTemplateVariables}
onSelectTemplate={this.handleSelectTemplate}
onOpenTemplateManager={this.handleOpenTemplateManager}
isOpen={showTemplateControlBar}
source={source}
/>
)}
{dashboard ? (
@ -504,7 +474,6 @@ class DashboardPage extends Component {
onDeleteCell={this.handleDeleteDashboardCell}
onCloneCell={this.handleCloneCell}
showTemplateControlBar={showTemplateControlBar}
onOpenTemplateManager={this.handleOpenTemplateManager}
templatesIncludingDashTime={templatesIncludingDashTime}
onSummonOverlayTechnologies={handleShowCellEditorOverlay}
/>

View File

@ -16,7 +16,7 @@ export const initialState = {
activeCellID: '',
}
import {TEMPLATE_VARIABLE_TYPES} from 'src/dashboards/constants'
import {TEMPLATE_VARIABLE_TYPES} from 'src/tempVars/constants'
const ui = (state = initialState, action) => {
switch (action.type) {

View File

@ -1,6 +1,5 @@
import _ from 'lodash'
import {TEMPLATE_VARIABLE_QUERIES} from 'src/dashboards/constants'
import {
Dashboard,
Template,
@ -8,65 +7,7 @@ import {
TemplateValue,
URLQueryParams,
} from 'src/types'
import {TemplateUpdate} from 'src/types/tempVars'
interface PartialTemplateWithQuery {
query: string
tempVars: Array<Partial<Template>>
}
const generateTemplateVariableQuery = ({
type,
query: {
database,
// rp, TODO
measurement,
tagKey,
},
}: Partial<Template>): PartialTemplateWithQuery => {
const tempVars = []
if (database) {
tempVars.push({
tempVar: ':database:',
values: [
{
type: 'database',
value: database,
},
],
})
}
if (measurement) {
tempVars.push({
tempVar: ':measurement:',
values: [
{
type: 'measurement',
value: measurement,
},
],
})
}
if (tagKey) {
tempVars.push({
tempVar: ':tagKey:',
values: [
{
type: 'tagKey',
value: tagKey,
},
],
})
}
const query: string = TEMPLATE_VARIABLE_QUERIES[type]
return {
query,
tempVars,
}
}
import {TemplateUpdate} from 'src/types'
export const makeQueryForTemplate = ({
influxql,
@ -97,7 +38,7 @@ export const generateURLQueryParamsFromTempVars = (
return urlQueryParams
}
export const isValidTempVarOverride = (
const isValidTempVarOverride = (
values: TemplateValue[],
overrideValue: string
): boolean => !!values.find(({value}) => value === overrideValue)
@ -199,5 +140,3 @@ export const findInvalidTempVarsInURLQuery = (
return urlQueryParamsTempVarsWithInvalidValues
}
export default generateTemplateVariableQuery

View File

@ -1,6 +1,6 @@
import _ from 'lodash'
import {fetchTimeSeriesAsync} from 'src/shared/actions/timeSeries'
import {removeUnselectedTemplateValues} from 'src/dashboards/constants'
import {removeUnselectedTemplateValues} from 'src/tempVars/constants'
import {intervalValuesPoints} from 'src/shared/constants'

View File

@ -0,0 +1,24 @@
import React, {SFC} from 'react'
import {RemoteDataState} from 'src/types'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import Dropdown from 'src/shared/components/Dropdown'
interface Props {
rds: RemoteDataState
children: typeof Dropdown
}
const DropdownLoadingPlaceholder: SFC<Props> = ({children, rds}) => {
if (rds === RemoteDataState.Loading) {
return (
<div className="dropdown-placeholder">
<LoadingSpinner />
</div>
)
}
return children
}
export default DropdownLoadingPlaceholder

View File

@ -0,0 +1,12 @@
import React, {SFC} from 'react'
const SimpleOverlayTechnology: SFC = ({children}) => {
return (
<div className="overlay-tech show">
<div className="overlay--dialog">{children}</div>
<div className="overlay--mask" />
</div>
)
}
export default SimpleOverlayTechnology

View File

@ -1,5 +1,6 @@
import _ from 'lodash'
import {TemplateValueType, TemplateType} from 'src/types'
import {CellType} from 'src/types/dashboard'
export const NO_CELL = 'none'
@ -440,12 +441,12 @@ export const DEFAULT_SOURCE = {
export const defaultIntervalValue = '333'
export const intervalValuesPoints = [
{value: defaultIntervalValue, type: 'points', selected: true},
{value: defaultIntervalValue, type: TemplateValueType.Points, selected: true},
]
export const interval = {
id: 'interval',
type: 'autoGroupBy',
type: TemplateType.AutoGroupBy,
tempVar: TEMP_VAR_INTERVAL,
label: 'automatically determine the best group by time',
values: intervalValuesPoints,

View File

@ -72,6 +72,7 @@
@import 'components/threesizer';
@import 'components/threshold-controls';
@import 'components/kapacitor-logs-table';
@import 'components/dropdown-placeholder';
// Pages
@import 'pages/config-endpoints';

View File

@ -0,0 +1,14 @@
.dropdown-placeholder {
background-color: $g5-pepper;
border-radius: 4px;
flex-grow: 1;
flex-shrink: 1;
display: flex;
align-items: center;
justify-content: center;
height: $form-md-height;
.loading-spinner .spinner div {
background-color: $g14-chromium;
}
}

View File

@ -0,0 +1,153 @@
$padding: 20px 30px;
.edit-temp-var {
margin: 0 auto;
width: 650px;
}
.edit-temp-var--header {
background-color: $g0-obsidian;
padding: $padding;
display: flex;
align-items: center;
justify-content: space-between;
}
.edit-temp-var--header-controls > button {
display: inline-block;
margin: 0 5px;
}
.edit-temp-var--body {
@include gradient-v($g3-castle, $g1-raven);
padding: $padding;
.delete {
margin: 20px auto 10px auto;
width: 90px;
}
}
.edit-temp-var--body-row {
display: flex;
.name {
flex: 1 1 50%;
padding-right: 10px;
input {
color: $c-potassium;
font-weight: bold;
}
}
.template-type {
flex: 1 1 50%;
padding-left: 10px;
}
.dropdown {
display: block;
width: 100%;
}
.dropdown-toggle, .dropdown-placeholder {
width: 100%;
}
}
.temp-builder {
width: 100%;
}
.temp-builder--mq-controls {
background: $g3-castle;
border-radius: $radius-small;
display: flex;
padding: 10px 10px 0 10px;
&:last-child {
padding-bottom: 10px;
}
> .temp-builder--mq-text, > .dropdown, .dropdown-placeholder {
margin-right: 5px;
flex-grow: 1;
&:last-child {
margin-right: 0;
}
}
}
.temp-builder--mq-text {
@include no-user-select();
background-color: $g5-pepper;
border-radius: $radius-small;
padding: 8px;
white-space: nowrap;
color: $c-pool;
font-size: 14px;
font-weight: 600;
font-family: $code-font;
}
.temp-builder .temp-builder-results {
margin-top: 30px;
}
@keyframes pulse {
0% {
color: $g19-ghost;
}
50% {
color: $g12-forge;
}
100% {
color: $g19-ghost;
}
}
.temp-builder-results > p {
text-align: center;
font-weight: bold;
color: $g19-ghost;
margin: 15px 0;
&.error {
color: $c-fire;
}
&.warning {
color: $c-pineapple;
}
&.loading {
animation: pulse 1.5s ease infinite;
}
> strong {
color: $c-potassium;
}
}
.temp-builder-results--list {
max-height: 250px;
padding: 0;
margin: 0;
li {
background-color: $g3-castle;
padding: 0 10px;
display: flex;
align-items: center;
border-radius: $radius-small;
margin: 0;
color: $g19-ghost;
font-weight: bold;
list-style: none;
}
}

View File

@ -62,7 +62,7 @@ button.btn.template-control--manage {
font-weight: 500;
@include no-user-select();
}
.template-control--dropdown {
.template-control--controls > .template-control--dropdown {
flex: 0 1 auto;
min-width: $template-control-dropdown-min-width;
max-width: $template-control-dropdown-max-width;
@ -71,29 +71,32 @@ button.btn.template-control--manage {
align-items: stretch;
margin: $template-control--margin;
.dropdown {
> .dropdown {
order: 2;
margin: 0;
flex: 1 0 0%;
.dropdown-toggle {
border-radius: 0 0 $radius-small $radius-small;
width: 100%;
font-size: 12px;
font-family: $code-font;
}
.dropdown-menu .fancy-scroll--view li.dropdown-item a {
white-space: pre-wrap;
word-break: break-all;
overflow: hidden;
font-family: $code-font;
font-size: 12px;
}
}
.dropdown-toggle {
border-radius: 0 0 $radius-small $radius-small;
width: 100%;
font-size: 12px;
font-family: $code-font;
}
.dropdown .dropdown-menu .fancy-scroll--view li.dropdown-item a {
white-space: pre-wrap;
word-break: break-all;
overflow: hidden;
font-family: $code-font;
font-size: 12px;
}
}
.template-control--label {
@include no-user-select();
order: 1;
height: 18px;
height: 22px;
padding: 0 8px;
margin: 0;
font-size: 11px;
@ -102,4 +105,13 @@ button.btn.template-control--manage {
line-height: 18px;
border-radius: $radius-small $radius-small 0 0;
background-color: $g4-onyx;
display: flex;
align-items: center;
justify-content: space-between;
> .icon {
padding-bottom: 1px;
color: $g11-sidewalk;
cursor: pointer;
}
}

View File

@ -495,6 +495,12 @@ $dash-graph-options-arrow: 8px;
*/
@import '../components/template-variables-manager';
/*
Edit Template Variable
------------------------------------------------------
*/
@import '../components/edit-template-variable';
/*
Write Data Form
------------------------------------------------------

View File

@ -0,0 +1,91 @@
import React, {PureComponent, ChangeEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplatePreviewList from 'src/tempVars/components/TemplatePreviewList'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
interface State {
templateValues: string[]
templateValuesString: string
}
@ErrorHandling
class CSVTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
public constructor(props) {
super(props)
const templateValues = props.template.values.map(v => v.value)
this.state = {
templateValues,
templateValuesString: templateValues.join(', '),
}
}
public render() {
const {templateValues, templateValuesString} = this.state
const pluralizer = templateValues.length === 1 ? '' : 's'
return (
<div className="temp-builder csv-temp-builder">
<div className="form-group">
<label>Comma Separated Values</label>
<div className="temp-builder--mq-controls">
<textarea
className="form-control"
value={templateValuesString}
onChange={this.handleChange}
onBlur={this.handleBlur}
/>
</div>
</div>
<div className="temp-builder-results">
<p>
CSV contains <strong>{templateValues.length}</strong> value{
pluralizer
}
</p>
{templateValues.length > 0 && (
<TemplatePreviewList items={templateValues} />
)}
</div>
</div>
)
}
private handleChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
this.setState({templateValuesString: e.target.value})
}
private handleBlur = (): void => {
const {template, onUpdateTemplate} = this.props
const {templateValuesString} = this.state
let templateValues
if (templateValuesString.trim() === '') {
templateValues = []
} else {
templateValues = templateValuesString.split(',').map(s => s.trim())
}
this.setState({templateValues})
const nextValues = templateValues.map(value => {
return {
type: TemplateValueType.CSV,
value,
selected: false,
}
})
if (nextValues.length > 0) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
}
}
export default CSVTemplateBuilder

View File

@ -0,0 +1,94 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {showDatabases} from 'src/shared/apis/metaQuery'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import {
TemplateBuilderProps,
RemoteDataState,
TemplateValueType,
} from 'src/types'
interface State {
databases: string[]
databasesStatus: RemoteDataState
}
@ErrorHandling
class DatabasesTemplateBuilder extends PureComponent<
TemplateBuilderProps,
State
> {
constructor(props) {
super(props)
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
this.loadDatabases()
}
public render() {
const {databases, databasesStatus} = this.state
return (
<div className="temp-builder databases-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">SHOW DATABASES</div>
</div>
</div>
<TemplateMetaQueryPreview
items={databases}
loadingStatus={databasesStatus}
/>
</div>
)
}
private async loadDatabases(): Promise<void> {
const {template, source, onUpdateTemplate} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
const nextValues = databases.map(db => {
return {
type: TemplateValueType.Database,
value: db,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
const nextTemplate = {
...template,
values: nextValues,
}
onUpdateTemplate(nextTemplate)
} catch {
this.setState({databasesStatus: RemoteDataState.Error})
}
}
}
export default DatabasesTemplateBuilder

View File

@ -0,0 +1,39 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
import KeysTemplateBuilder from 'src/tempVars/components/KeysTemplateBuilder'
import {proxy} from 'src/utils/queryUrlGenerator'
import parseShowFieldKeys from 'src/shared/parsing/showFieldKeys'
const fetchKeys = async (source, db, measurement): Promise<string[]> => {
const {data} = await proxy({
source: source.links.proxy,
db,
query: `SHOW FIELD KEYS ON "${db}" FROM "${measurement}"`,
})
const {fieldSets} = parseShowFieldKeys(data)
const fieldKeys = _.get(Object.values(fieldSets), '0', [])
return fieldKeys
}
class FieldKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
public render() {
const {template, source, onUpdateTemplate} = this.props
return (
<KeysTemplateBuilder
queryPrefix={'SHOW FIELD KEYS ON'}
templateValueType={TemplateValueType.FieldKey}
fetchKeys={fetchKeys}
template={template}
source={source}
onUpdateTemplate={onUpdateTemplate}
/>
)
}
}
export default FieldKeysTemplateBuilder

View File

@ -0,0 +1,244 @@
import React, {PureComponent} from 'react'
import {getDeep} from 'src/utils/wrappers'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import {showDatabases, showMeasurements} from 'src/shared/apis/metaQuery'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import parseShowMeasurements from 'src/shared/parsing/showMeasurements'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import DropdownLoadingPlaceholder from 'src/shared/components/DropdownLoadingPlaceholder'
import {
TemplateBuilderProps,
TemplateValueType,
RemoteDataState,
Source,
} from 'src/types'
interface Props extends TemplateBuilderProps {
queryPrefix: string
templateValueType: TemplateValueType
fetchKeys: (
source: Source,
db: string,
measurement: string
) => Promise<string[]>
}
interface State {
databases: string[]
databasesStatus: RemoteDataState
selectedDatabase: string
measurements: string[]
measurementsStatus: RemoteDataState
selectedMeasurement: string
keys: string[]
keysStatus: RemoteDataState
}
@ErrorHandling
class KeysTemplateBuilder extends PureComponent<Props, State> {
constructor(props) {
super(props)
const selectedDatabase = getDeep(props, 'template.query.db', '')
const selectedMeasurement = getDeep(props, 'template.query.measurement', '')
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
selectedDatabase,
measurements: [],
measurementsStatus: RemoteDataState.Loading,
selectedMeasurement,
keys: [],
keysStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
await this.loadDatabases()
await this.loadMeasurements()
await this.loadKeys()
}
public render() {
const {queryPrefix} = this.props
const {
databases,
databasesStatus,
selectedDatabase,
measurements,
measurementsStatus,
selectedMeasurement,
keys,
keysStatus,
} = this.state
return (
<div className="temp-builder measurements-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">{queryPrefix}</div>
<DropdownLoadingPlaceholder rds={databasesStatus}>
<Dropdown
items={databases.map(text => ({text}))}
onChoose={this.handleChooseDatabaseDropdown}
selected={selectedDatabase}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
<div className="temp-builder--mq-text">FROM</div>
<DropdownLoadingPlaceholder rds={measurementsStatus}>
<Dropdown
items={measurements.map(text => ({text}))}
onChoose={this.handleChooseMeasurementDropdown}
selected={selectedMeasurement}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
</div>
<TemplateMetaQueryPreview items={keys} loadingStatus={keysStatus} />
</div>
)
}
private async loadDatabases(): Promise<void> {
const {source} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
const {selectedDatabase} = this.state
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
if (!selectedDatabase) {
this.handleChooseDatabase(getDeep(databases, 0, ''))
}
} catch (error) {
this.setState({databasesStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadMeasurements(): Promise<void> {
const {source} = this.props
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({measurementsStatus: RemoteDataState.Loading})
try {
const {data} = await showMeasurements(
source.links.proxy,
selectedDatabase
)
const {measurementSets} = parseShowMeasurements(data)
const measurements = getDeep(measurementSets, '0.measurements', [])
this.setState({
measurements,
measurementsStatus: RemoteDataState.Done,
})
if (!selectedMeasurement) {
this.handleChooseMeasurement(getDeep(measurements, 0, ''))
}
} catch (error) {
this.setState({measurementsStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadKeys(): Promise<void> {
const {
template,
onUpdateTemplate,
templateValueType,
fetchKeys,
source,
} = this.props
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({keysStatus: RemoteDataState.Loading})
try {
const keys = await fetchKeys(
source,
selectedDatabase,
selectedMeasurement
)
this.setState({
keys,
keysStatus: RemoteDataState.Done,
})
const nextValues = keys.map(value => {
return {
type: templateValueType,
value,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
} catch (error) {
this.setState({keysStatus: RemoteDataState.Error})
console.error(error)
}
}
private handleChooseDatabaseDropdown = ({text}) => {
this.handleChooseDatabase(text)
}
private handleChooseDatabase = (db: string): void => {
this.setState({selectedDatabase: db, selectedMeasurement: ''}, () =>
this.loadMeasurements()
)
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
db,
},
})
}
private handleChooseMeasurementDropdown = ({text}): void => {
this.handleChooseMeasurement(text)
}
private handleChooseMeasurement = (measurement: string): void => {
this.setState({selectedMeasurement: measurement}, () => this.loadKeys())
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
measurement,
},
})
}
}
export default KeysTemplateBuilder

View File

@ -0,0 +1,165 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import {showDatabases, showMeasurements} from 'src/shared/apis/metaQuery'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import parseShowMeasurements from 'src/shared/parsing/showMeasurements'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import DropdownLoadingPlaceholder from 'src/shared/components/DropdownLoadingPlaceholder'
import {
TemplateBuilderProps,
RemoteDataState,
TemplateValueType,
} from 'src/types'
interface State {
databases: string[]
databasesStatus: RemoteDataState
selectedDatabase: string
measurements: string[]
measurementsStatus: RemoteDataState
}
@ErrorHandling
class MeasurementsTemplateBuilder extends PureComponent<
TemplateBuilderProps,
State
> {
constructor(props) {
super(props)
const selectedDatabase = _.get(props, 'template.query.db', '')
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
selectedDatabase,
measurements: [],
measurementsStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
await this.loadDatabases()
await this.loadMeasurements()
}
public render() {
const {
databases,
databasesStatus,
selectedDatabase,
measurements,
measurementsStatus,
} = this.state
return (
<div className="temp-builder measurements-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">SHOW MEASUREMENTS ON</div>
<DropdownLoadingPlaceholder rds={databasesStatus}>
<Dropdown
items={databases.map(text => ({text}))}
onChoose={this.handleChooseDatabaseDropdown}
selected={selectedDatabase}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
</div>
<TemplateMetaQueryPreview
items={measurements}
loadingStatus={measurementsStatus}
/>
</div>
)
}
private async loadDatabases(): Promise<void> {
const {source} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
const {selectedDatabase} = this.state
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
if (!selectedDatabase) {
this.handleChooseDatabase(_.get(databases, 0, ''))
}
} catch (error) {
this.setState({databasesStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadMeasurements(): Promise<void> {
const {template, source, onUpdateTemplate} = this.props
const {selectedDatabase} = this.state
this.setState({measurementsStatus: RemoteDataState.Loading})
try {
const {data} = await showMeasurements(
source.links.proxy,
selectedDatabase
)
const {measurementSets} = parseShowMeasurements(data)
const measurements = _.get(measurementSets, '0.measurements', [])
this.setState({
measurements,
measurementsStatus: RemoteDataState.Done,
})
const nextValues = measurements.map(value => {
return {
type: TemplateValueType.Measurement,
value,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
} catch (error) {
this.setState({measurementsStatus: RemoteDataState.Error})
console.error(error)
}
}
private handleChooseDatabaseDropdown = ({text}) => {
this.handleChooseDatabase(text)
}
private handleChooseDatabase = (db: string): void => {
this.setState({selectedDatabase: db}, () => this.loadMeasurements())
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
db,
},
})
}
}
export default MeasurementsTemplateBuilder

View File

@ -0,0 +1,42 @@
import React, {PureComponent} from 'react'
import KeysTemplateBuilder from 'src/tempVars/components/KeysTemplateBuilder'
import {proxy} from 'src/utils/queryUrlGenerator'
import parseShowTagKeys from 'src/shared/parsing/showTagKeys'
import {TemplateBuilderProps, TemplateValueType} from 'src/types'
export const fetchTagKeys = async (
source,
db,
measurement
): Promise<string[]> => {
const {data} = await proxy({
source: source.links.proxy,
db,
query: `SHOW TAG KEYS ON "${db}" FROM "${measurement}"`,
})
const {tagKeys} = parseShowTagKeys(data)
return tagKeys
}
class TagKeysTemplateBuilder extends PureComponent<TemplateBuilderProps> {
public render() {
const {template, source, onUpdateTemplate} = this.props
return (
<KeysTemplateBuilder
queryPrefix={'SHOW TAG KEYS ON'}
templateValueType={TemplateValueType.TagKey}
fetchKeys={fetchTagKeys}
template={template}
source={source}
onUpdateTemplate={onUpdateTemplate}
/>
)
}
}
export default TagKeysTemplateBuilder

View File

@ -0,0 +1,308 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import {showDatabases, showMeasurements} from 'src/shared/apis/metaQuery'
import {proxy} from 'src/utils/queryUrlGenerator'
import parseShowDatabases from 'src/shared/parsing/showDatabases'
import parseShowMeasurements from 'src/shared/parsing/showMeasurements'
import parseShowTagValues from 'src/shared/parsing/showTagValues'
import {fetchTagKeys} from 'src/tempVars/components/TagKeysTemplateBuilder'
import TemplateMetaQueryPreview from 'src/tempVars/components/TemplateMetaQueryPreview'
import DropdownLoadingPlaceholder from 'src/shared/components/DropdownLoadingPlaceholder'
import {
TemplateBuilderProps,
TemplateValueType,
RemoteDataState,
} from 'src/types'
interface State {
databases: string[]
databasesStatus: RemoteDataState
selectedDatabase: string
measurements: string[]
measurementsStatus: RemoteDataState
selectedMeasurement: string
tagKeys: string[]
tagKeysStatus: RemoteDataState
selectedTagKey: string
tagValues: string[]
tagValuesStatus: RemoteDataState
}
@ErrorHandling
class KeysTemplateBuilder extends PureComponent<TemplateBuilderProps, State> {
constructor(props) {
super(props)
const selectedDatabase = _.get(props, 'template.query.db', '')
const selectedMeasurement = _.get(props, 'template.query.measurement', '')
const selectedTagKey = _.get(props, 'template.query.tagKey', '')
this.state = {
databases: [],
databasesStatus: RemoteDataState.Loading,
selectedDatabase,
measurements: [],
measurementsStatus: RemoteDataState.Loading,
selectedMeasurement,
tagKeys: [],
tagKeysStatus: RemoteDataState.Loading,
selectedTagKey,
tagValues: [],
tagValuesStatus: RemoteDataState.Loading,
}
}
public async componentDidMount() {
await this.loadDatabases()
await this.loadMeasurements()
await this.loadTagKeys()
await this.loadTagValues()
}
public render() {
const {
databases,
databasesStatus,
selectedDatabase,
measurements,
measurementsStatus,
selectedMeasurement,
tagKeys,
tagKeysStatus,
selectedTagKey,
tagValues,
tagValuesStatus,
} = this.state
return (
<div className="temp-builder measurements-temp-builder">
<div className="form-group">
<label>Meta Query</label>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">SHOW TAG VALUES ON</div>
<DropdownLoadingPlaceholder rds={databasesStatus}>
<Dropdown
items={databases.map(text => ({text}))}
onChoose={this.handleChooseDatabaseDropdown}
selected={selectedDatabase}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
<div className="temp-builder--mq-controls">
<div className="temp-builder--mq-text">FROM</div>
<DropdownLoadingPlaceholder rds={measurementsStatus}>
<Dropdown
items={measurements.map(text => ({text}))}
onChoose={this.handleChooseMeasurementDropdown}
selected={selectedMeasurement}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
<div className="temp-builder--mq-text">WITH KEY</div>
<DropdownLoadingPlaceholder rds={tagKeysStatus}>
<Dropdown
items={tagKeys.map(text => ({text}))}
onChoose={this.handleChooseTagKeyDropdown}
selected={selectedTagKey}
buttonSize=""
/>
</DropdownLoadingPlaceholder>
</div>
</div>
<TemplateMetaQueryPreview
items={tagValues}
loadingStatus={tagValuesStatus}
/>
</div>
)
}
private async loadDatabases(): Promise<void> {
const {source} = this.props
this.setState({databasesStatus: RemoteDataState.Loading})
try {
const {data} = await showDatabases(source.links.proxy)
const {databases} = parseShowDatabases(data)
const {selectedDatabase} = this.state
this.setState({
databases,
databasesStatus: RemoteDataState.Done,
})
if (!selectedDatabase) {
this.handleChooseDatabase(_.get(databases, 0, ''))
}
} catch (error) {
this.setState({databasesStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadMeasurements(): Promise<void> {
const {source} = this.props
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({measurementsStatus: RemoteDataState.Loading})
try {
const {data} = await showMeasurements(
source.links.proxy,
selectedDatabase
)
const {measurementSets} = parseShowMeasurements(data)
const measurements = _.get(measurementSets, '0.measurements', [])
this.setState({
measurements,
measurementsStatus: RemoteDataState.Done,
})
if (!selectedMeasurement) {
this.handleChooseMeasurement(_.get(measurements, 0, ''))
}
} catch (error) {
this.setState({measurementsStatus: RemoteDataState.Error})
console.error(error)
}
}
private async loadTagKeys(): Promise<void> {
const {source} = this.props
const {selectedTagKey} = this.state
const {selectedDatabase, selectedMeasurement} = this.state
this.setState({tagKeysStatus: RemoteDataState.Loading})
try {
const tagKeys = await fetchTagKeys(
source,
selectedDatabase,
selectedMeasurement
)
this.setState({
tagKeys,
tagKeysStatus: RemoteDataState.Done,
})
if (!selectedTagKey) {
this.handleChooseTagKey(_.get(tagKeys, 0, ''))
}
} catch (error) {
this.setState({tagKeysStatus: RemoteDataState.Error})
console.error(error)
}
}
private loadTagValues = async (): Promise<void> => {
const {source, template, onUpdateTemplate} = this.props
const {selectedDatabase, selectedMeasurement, selectedTagKey} = this.state
this.setState({tagValuesStatus: RemoteDataState.Loading})
try {
const {data} = await proxy({
source: source.links.proxy,
db: selectedDatabase,
query: `SHOW TAG VALUES ON "${selectedDatabase}" FROM "${selectedMeasurement}" WITH KEY = "${selectedTagKey}"`,
})
const {tags} = parseShowTagValues(data)
const tagValues = _.get(Object.values(tags), 0, [])
this.setState({
tagValues,
tagValuesStatus: RemoteDataState.Done,
})
const nextValues = tagValues.map(value => {
return {
type: TemplateValueType.TagValue,
value,
selected: false,
}
})
if (nextValues[0]) {
nextValues[0].selected = true
}
onUpdateTemplate({...template, values: nextValues})
} catch (error) {
this.setState({tagValuesStatus: RemoteDataState.Error})
console.error(error)
}
}
private handleChooseDatabaseDropdown = ({text}) => {
this.handleChooseDatabase(text)
}
private handleChooseDatabase = (db: string): void => {
this.setState({selectedDatabase: db, selectedMeasurement: ''}, () =>
this.loadMeasurements()
)
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
db,
tagKey: '',
measurement: '',
},
})
}
private handleChooseMeasurementDropdown = ({text}): void => {
this.handleChooseMeasurement(text)
}
private handleChooseMeasurement = (measurement: string): void => {
this.setState({selectedMeasurement: measurement, selectedTagKey: ''}, () =>
this.loadTagKeys()
)
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
measurement,
tagKey: '',
},
})
}
private handleChooseTagKeyDropdown = ({text}): void => {
this.handleChooseTagKey(text)
}
private handleChooseTagKey = (tagKey: string): void => {
this.setState({selectedTagKey: tagKey}, () => this.loadTagValues())
const {template, onUpdateTemplate} = this.props
onUpdateTemplate({
...template,
query: {
...template.query,
tagKey,
},
})
}
}
export default KeysTemplateBuilder

View File

@ -0,0 +1,129 @@
import React, {Component} from 'react'
import classnames from 'classnames'
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
import Authorized, {EDITOR_ROLE} from 'src/auth/Authorized'
import {Template, Source} from 'src/types'
interface Props {
meRole: string
isUsingAuth: boolean
templates: Template[]
isOpen: boolean
source: Source
onSelectTemplate: (id: string) => void
onSaveTemplates: (templates: Template[]) => void
onCreateTemplateVariable: () => void
}
interface State {
isAdding: boolean
}
class TemplateControlBar extends Component<Props, State> {
constructor(props) {
super(props)
this.state = {isAdding: false}
}
public render() {
const {
isOpen,
templates,
onSelectTemplate,
meRole,
isUsingAuth,
source,
} = this.props
const {isAdding} = this.state
return (
<div className={classnames('template-control-bar', {show: isOpen})}>
<div className="template-control--container">
<div className="template-control--controls">
{templates && templates.length ? (
templates.map(template => (
<TemplateControlDropdown
key={template.id}
meRole={meRole}
isUsingAuth={isUsingAuth}
template={template}
source={source}
onSelectTemplate={onSelectTemplate}
onCreateTemplate={this.handleCreateTemplate}
onUpdateTemplate={this.handleUpdateTemplate}
onDeleteTemplate={this.handleDeleteTemplate}
/>
))
) : (
<div className="template-control--empty" data-test="empty-state">
This dashboard does not have any{' '}
<strong>Template Variables</strong>
</div>
)}
{isAdding && (
<SimpleOverlayTechnology>
<TemplateVariableEditor
source={source}
onCreate={this.handleCreateTemplate}
onCancel={this.handleCancelAddVariable}
/>
</SimpleOverlayTechnology>
)}
</div>
<Authorized requiredRole={EDITOR_ROLE}>
<button
className="btn btn-primary btn-sm template-control--manage"
onClick={this.handleAddVariable}
>
<span className="icon plus" />
Add Variable
</button>
</Authorized>
</div>
</div>
)
}
private handleAddVariable = (): void => {
this.setState({isAdding: true})
}
private handleCancelAddVariable = (): void => {
this.setState({isAdding: false})
}
private handleCreateTemplate = async (template: Template): Promise<void> => {
const {templates, onSaveTemplates} = this.props
await onSaveTemplates([...templates, template])
this.setState({isAdding: false})
}
private handleUpdateTemplate = async (template: Template): Promise<void> => {
const {templates, onSaveTemplates} = this.props
const newTemplates = templates.reduce((acc, t) => {
if (t.id === template.id) {
return [...acc, template]
}
return [...acc, t]
}, [])
await onSaveTemplates(newTemplates)
}
private handleDeleteTemplate = async (template: Template): Promise<void> => {
const {templates, onSaveTemplates} = this.props
const newTemplates = templates.filter(t => t.id !== template.id)
await onSaveTemplates(newTemplates)
}
}
export default TemplateControlBar

View File

@ -0,0 +1,117 @@
import React, {PureComponent} from 'react'
import Dropdown from 'src/shared/components/Dropdown'
import SimpleOverlayTechnology from 'src/shared/components/SimpleOverlayTechnology'
import TemplateVariableEditor from 'src/tempVars/components/TemplateVariableEditor'
import {calculateDropdownWidth} from 'src/dashboards/constants/templateControlBar'
import Authorized, {isUserAuthorized, EDITOR_ROLE} from 'src/auth/Authorized'
import {Template, Source} from 'src/types'
interface Props {
template: Template
meRole: string
isUsingAuth: boolean
source: Source
onSelectTemplate: (id: string) => void
onCreateTemplate: (template: Template) => Promise<void>
onUpdateTemplate: (template: Template) => Promise<void>
onDeleteTemplate: (template: Template) => Promise<void>
}
interface State {
isEditing: boolean
}
class TemplateControlDropdown extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isEditing: false,
}
}
public render() {
const {
template,
isUsingAuth,
meRole,
source,
onSelectTemplate,
onCreateTemplate,
} = this.props
const {isEditing} = this.state
const dropdownItems = template.values.map(value => ({
...value,
text: value.value,
}))
const dropdownStyle = template.values.length
? {minWidth: calculateDropdownWidth(template.values)}
: null
const selectedItem = dropdownItems.find(item => item.selected) ||
dropdownItems[0] || {text: '(No values)'}
return (
<div className="template-control--dropdown" style={dropdownStyle}>
<Dropdown
items={dropdownItems}
buttonSize="btn-xs"
menuClass="dropdown-astronaut"
useAutoComplete={true}
selected={selectedItem.text}
disabled={isUsingAuth && !isUserAuthorized(meRole, EDITOR_ROLE)}
onChoose={onSelectTemplate(template.id)}
/>
<Authorized requiredRole={EDITOR_ROLE}>
<label className="template-control--label">
{template.tempVar}
<span
className="icon cog-thick"
onClick={this.handleShowSettings}
/>
</label>
</Authorized>
{isEditing && (
<SimpleOverlayTechnology>
<TemplateVariableEditor
template={template}
source={source}
onCreate={onCreateTemplate}
onUpdate={this.handleUpdateTemplate}
onDelete={this.handleDelete}
onCancel={this.handleHideSettings}
/>
</SimpleOverlayTechnology>
)}
</div>
)
}
private handleShowSettings = (): void => {
this.setState({isEditing: true})
}
private handleHideSettings = (): void => {
this.setState({isEditing: false})
}
private handleUpdateTemplate = async (template: Template): Promise<void> => {
const {onUpdateTemplate} = this.props
await onUpdateTemplate(template)
this.setState({isEditing: false})
}
private handleDelete = (): Promise<any> => {
const {onDeleteTemplate, template} = this.props
return onDeleteTemplate(template)
}
}
export default TemplateControlDropdown

View File

@ -0,0 +1,61 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TemplatePreviewList from 'src/tempVars/components/TemplatePreviewList'
import {RemoteDataState} from 'src/types'
interface Props {
items: string[]
loadingStatus: RemoteDataState
}
@ErrorHandling
class TemplateMetaQueryPreview extends PureComponent<Props> {
public render() {
const {items, loadingStatus} = this.props
if (loadingStatus === RemoteDataState.NotStarted) {
return <div className="temp-builder-results" />
}
if (loadingStatus === RemoteDataState.Loading) {
return (
<div className="temp-builder-results">
<p className="loading">Loading meta query preview...</p>
</div>
)
}
if (loadingStatus === RemoteDataState.Error) {
return (
<div className="temp-builder-results">
<p className="error">Meta Query failed to execute</p>
</div>
)
}
if (items.length === 0) {
return (
<div className="temp-builder-results">
<p className="warning">
Meta Query is syntactically correct but returned no results
</p>
</div>
)
}
const pluralizer = items.length === 1 ? '' : 's'
return (
<div className="temp-builder-results">
<p>
Meta Query returned <strong>{items.length}</strong> value{pluralizer}
</p>
{items.length > 0 && <TemplatePreviewList items={items} />}
</div>
)
}
}
export default TemplateMetaQueryPreview

View File

@ -0,0 +1,52 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FancyScrollbar from 'src/shared/components/FancyScrollbar'
const LI_HEIGHT = 35
const LI_MARGIN_BOTTOM = 3
const RESULTS_TO_DISPLAY = 10
interface Props {
items: string[]
}
@ErrorHandling
class TemplatePreviewList extends PureComponent<Props> {
public render() {
const {items} = this.props
return (
<ul
className="temp-builder-results--list"
style={{height: `${this.resultsListHeight}px`}}
>
<FancyScrollbar>
{items.map(db => {
return (
<li
key={uuid.v4()}
style={{
height: `${LI_HEIGHT}px`,
marginBottom: `${LI_MARGIN_BOTTOM}px`,
}}
>
{db}
</li>
)
})}
</FancyScrollbar>
</ul>
)
}
private get resultsListHeight() {
const {items} = this.props
const count = Math.min(items.length, RESULTS_TO_DISPLAY)
return count * (LI_HEIGHT + LI_MARGIN_BOTTOM)
}
}
export default TemplatePreviewList

View File

@ -0,0 +1,288 @@
import React, {
PureComponent,
ComponentClass,
ChangeEvent,
KeyboardEvent,
} from 'react'
import {connect} from 'react-redux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import Dropdown from 'src/shared/components/Dropdown'
import ConfirmButton from 'src/shared/components/ConfirmButton'
import {getDeep} from 'src/utils/wrappers'
import {notify as notifyActionCreator} from 'src/shared/actions/notifications'
import DatabasesTemplateBuilder from 'src/tempVars/components/DatabasesTemplateBuilder'
import CSVTemplateBuilder from 'src/tempVars/components/CSVTemplateBuilder'
import MeasurementsTemplateBuilder from 'src/tempVars/components/MeasurementsTemplateBuilder'
import FieldKeysTemplateBuilder from 'src/tempVars/components/FieldKeysTemplateBuilder'
import TagKeysTemplateBuilder from 'src/tempVars/components/TagKeysTemplateBuilder'
import TagValuesTemplateBuilder from 'src/tempVars/components/TagValuesTemplateBuilder'
import {
Template,
TemplateType,
TemplateBuilderProps,
Source,
RemoteDataState,
Notification,
} from 'src/types'
import {
TEMPLATE_TYPES_LIST,
DEFAULT_TEMPLATES,
RESERVED_TEMPLATE_NAMES,
} from 'src/tempVars/constants'
import {FIVE_SECONDS} from 'src/shared/constants/index'
interface Props {
// We will assume we are creating a new template if none is passed in
template?: Template
source: Source
onCancel: () => void
onCreate?: (template: Template) => Promise<any>
onUpdate?: (template: Template) => Promise<any>
onDelete?: () => Promise<any>
notify: (n: Notification) => void
}
interface State {
nextTemplate: Template
isNew: boolean
savingStatus: RemoteDataState
deletingStatus: RemoteDataState
}
const TEMPLATE_BUILDERS = {
[TemplateType.Databases]: DatabasesTemplateBuilder,
[TemplateType.CSV]: CSVTemplateBuilder,
[TemplateType.Measurements]: MeasurementsTemplateBuilder,
[TemplateType.FieldKeys]: FieldKeysTemplateBuilder,
[TemplateType.TagKeys]: TagKeysTemplateBuilder,
[TemplateType.TagValues]: TagValuesTemplateBuilder,
}
const formatName = name => `:${name.replace(/:/g, '')}:`
const DEFAULT_TEMPLATE = DEFAULT_TEMPLATES[TemplateType.Databases]
@ErrorHandling
class TemplateVariableEditor extends PureComponent<Props, State> {
constructor(props) {
super(props)
const defaultState = {
savingStatus: RemoteDataState.NotStarted,
deletingStatus: RemoteDataState.NotStarted,
}
const {template} = this.props
if (template) {
this.state = {
...defaultState,
nextTemplate: {...template},
isNew: false,
}
} else {
this.state = {
...defaultState,
nextTemplate: DEFAULT_TEMPLATE(),
isNew: true,
}
}
}
public render() {
const {source, onCancel} = this.props
const {nextTemplate, isNew} = this.state
const TemplateBuilder = this.templateBuilder
return (
<div className="edit-temp-var">
<div className="edit-temp-var--header">
<h1 className="page-header__title">Edit Template Variable</h1>
<div className="edit-temp-var--header-controls">
<button
className="btn btn-default"
type="button"
onClick={onCancel}
>
Cancel
</button>
<button
className="btn btn-success"
type="button"
onClick={this.handleSave}
disabled={!this.canSave}
>
{this.isSaving ? 'Saving...' : 'Save'}
</button>
</div>
</div>
<div className="edit-temp-var--body">
<div className="edit-temp-var--body-row">
<div className="form-group name">
<label>Name</label>
<input
type="text"
className="form-control"
value={nextTemplate.tempVar}
onChange={this.handleChangeName}
onKeyPress={this.handleNameKeyPress}
onBlur={this.formatName}
/>
</div>
<div className="form-group template-type">
<label>Type</label>
<Dropdown
items={TEMPLATE_TYPES_LIST}
onChoose={this.handleChooseType}
selected={this.dropdownSelection}
buttonSize=""
/>
</div>
</div>
<div className="edit-temp-var--body-row">
<TemplateBuilder
template={nextTemplate}
source={source}
onUpdateTemplate={this.handleUpdateTemplate}
/>
</div>
<ConfirmButton
text={this.isDeleting ? 'Deleting...' : 'Delete'}
confirmAction={this.handleDelete}
type="btn-danger"
size="btn-xs"
customClass="delete"
disabled={isNew || this.isDeleting}
/>
</div>
</div>
)
}
private get templateBuilder(): ComponentClass<TemplateBuilderProps> {
const {
nextTemplate: {type},
} = this.state
const component = TEMPLATE_BUILDERS[type]
if (!component) {
throw new Error(`Could not find template builder for type "${type}"`)
}
return component
}
private handleUpdateTemplate = (nextTemplate: Template): void => {
this.setState({nextTemplate})
}
private handleChooseType = ({type}) => {
const {
nextTemplate: {id},
} = this.state
const nextNextTemplate = {...DEFAULT_TEMPLATES[type](), id}
this.setState({nextTemplate: nextNextTemplate})
}
private handleChangeName = (e: ChangeEvent<HTMLInputElement>): void => {
const {value} = e.target
const {nextTemplate} = this.state
this.setState({
nextTemplate: {
...nextTemplate,
tempVar: value,
},
})
}
private formatName = (): void => {
const {nextTemplate} = this.state
const tempVar = formatName(nextTemplate.tempVar)
this.setState({nextTemplate: {...nextTemplate, tempVar}})
}
private handleSave = async (): Promise<any> => {
if (!this.canSave) {
return
}
const {onUpdate, onCreate, notify} = this.props
const {nextTemplate, isNew} = this.state
nextTemplate.tempVar = formatName(nextTemplate.tempVar)
this.setState({savingStatus: RemoteDataState.Loading})
try {
if (isNew) {
await onCreate(nextTemplate)
} else {
await onUpdate(nextTemplate)
}
} catch (error) {
notify({
message: `Error saving template: ${error}`,
type: 'error',
icon: 'alert-triangle',
duration: FIVE_SECONDS,
})
console.error(error)
}
}
private handleNameKeyPress = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
this.handleSave()
}
}
private get isSaving(): boolean {
return this.state.savingStatus === RemoteDataState.Loading
}
private get canSave(): boolean {
const {
nextTemplate: {tempVar},
} = this.state
return (
tempVar !== '' &&
!RESERVED_TEMPLATE_NAMES.includes(formatName(tempVar)) &&
!this.isSaving
)
}
private get dropdownSelection(): string {
const {
nextTemplate: {type},
} = this.state
return getDeep(
TEMPLATE_TYPES_LIST.filter(t => t.type === type),
'0.text',
''
)
}
private handleDelete = (): void => {
const {onDelete} = this.props
this.setState({deletingStatus: RemoteDataState.Loading}, onDelete)
}
private get isDeleting(): boolean {
return this.state.deletingStatus === RemoteDataState.Loading
}
}
const mapDispatchToProps = {notify: notifyActionCreator}
export default connect(null, mapDispatchToProps)(TemplateVariableEditor)

View File

@ -0,0 +1,173 @@
import uuid from 'uuid'
import {TimeRange} from 'src/types/query'
import {TEMP_VAR_DASHBOARD_TIME} from 'src/shared/constants'
import {Template, TemplateType, TemplateValueType} from 'src/types'
interface TemplateTypesListItem {
text: string
type: TemplateType
}
export const TEMPLATE_TYPES_LIST: TemplateTypesListItem[] = [
{
text: 'Databases',
type: TemplateType.Databases,
},
{
text: 'Measurements',
type: TemplateType.Measurements,
},
{
text: 'Field Keys',
type: TemplateType.FieldKeys,
},
{
text: 'Tag Keys',
type: TemplateType.TagKeys,
},
{
text: 'Tag Values',
type: TemplateType.TagValues,
},
{
text: 'CSV',
type: TemplateType.CSV,
},
]
export const TEMPLATE_VARIABLE_TYPES = {
[TemplateType.CSV]: TemplateValueType.CSV,
[TemplateType.Databases]: TemplateValueType.Database,
[TemplateType.Measurements]: TemplateValueType.Measurement,
[TemplateType.FieldKeys]: TemplateValueType.FieldKey,
[TemplateType.TagKeys]: TemplateValueType.TagKey,
[TemplateType.TagValues]: TemplateValueType.TagValue,
}
export const TEMPLATE_VARIABLE_QUERIES = {
[TemplateType.Databases]: 'SHOW DATABASES',
[TemplateType.Measurements]: 'SHOW MEASUREMENTS ON :database:',
[TemplateType.FieldKeys]: 'SHOW FIELD KEYS ON :database: FROM :measurement:',
[TemplateType.TagKeys]: 'SHOW TAG KEYS ON :database: FROM :measurement:',
[TemplateType.TagValues]:
'SHOW TAG VALUES ON :database: FROM :measurement: WITH KEY=:tagKey:',
}
interface DefaultTemplates {
[templateType: string]: () => Template
}
export const DEFAULT_TEMPLATES: DefaultTemplates = {
[TemplateType.Databases]: () => {
return {
id: uuid.v4(),
tempVar: ':my-databases:',
values: [
{
value: '_internal',
type: TemplateValueType.Database,
selected: true,
},
],
type: TemplateType.Databases,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.Databases],
},
}
},
[TemplateType.Measurements]: () => {
return {
id: uuid.v4(),
tempVar: ':my-measurements:',
values: [],
type: TemplateType.Measurements,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.Measurements],
db: '',
},
}
},
[TemplateType.CSV]: () => {
return {
id: uuid.v4(),
tempVar: ':my-values:',
values: [],
type: TemplateType.CSV,
label: '',
}
},
[TemplateType.TagKeys]: () => {
return {
id: uuid.v4(),
tempVar: ':my-tag-keys:',
values: [],
type: TemplateType.TagKeys,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.TagKeys],
},
}
},
[TemplateType.FieldKeys]: () => {
return {
id: uuid.v4(),
tempVar: ':my-field-keys:',
values: [],
type: TemplateType.FieldKeys,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.FieldKeys],
},
}
},
[TemplateType.TagValues]: () => {
return {
id: uuid.v4(),
tempVar: ':my-tag-values:',
values: [],
type: TemplateType.TagValues,
label: '',
query: {
influxql: TEMPLATE_VARIABLE_QUERIES[TemplateType.TagValues],
},
}
},
}
export const RESERVED_TEMPLATE_NAMES = [
':dashboardTime:',
':upperDashboardTime:',
':interval:',
':lower:',
':upper:',
':zoomedLower:',
':zoomedUpper:',
]
export const MATCH_INCOMPLETE_TEMPLATES = /:[\w-]*/g
export const applyMasks = query => {
const matchWholeTemplates = /:([\w-]*):/g
const maskForWholeTemplates = '😸$1😸'
return query.replace(matchWholeTemplates, maskForWholeTemplates)
}
export const insertTempVar = (query, tempVar) => {
return query.replace(MATCH_INCOMPLETE_TEMPLATES, tempVar)
}
export const unMask = query => {
return query.replace(/😸/g, ':')
}
export const removeUnselectedTemplateValues = templates => {
return templates.map(template => {
const selectedValues = template.values.filter(value => value.selected)
return {...template, values: selectedValues}
})
}
export const TEMPLATE_RANGE: TimeRange = {
upper: null,
lower: TEMP_VAR_DASHBOARD_TIME,
}

View File

@ -1,6 +1,6 @@
import {QueryConfig} from 'src/types'
import {ColorString} from 'src/types/colors'
import {Template} from 'src/types/tempVars'
import {Template} from 'src/types'
export interface Axis {
bounds: [string, string]

View File

@ -7,6 +7,10 @@ import {
TemplateQuery,
TemplateValue,
URLQueryParams,
TemplateType,
TemplateValueType,
TemplateUpdate,
TemplateBuilderProps,
} from './tempVars'
import {
GroupBy,
@ -99,4 +103,8 @@ export {
RemoteDataState,
URLQueryParams,
AnnotationInterface,
TemplateType,
TemplateValueType,
TemplateUpdate,
TemplateBuilderProps,
}

View File

@ -1,25 +1,48 @@
import {Source} from 'src/types'
export enum TemplateValueType {
Database = 'database',
TagKey = 'tagKey',
FieldKey = 'fieldKey',
Measurement = 'measurement',
TagValue = 'tagValue',
CSV = 'csv',
Points = 'points',
Constant = 'constant',
}
export interface TemplateValue {
value: string
type: string
type: TemplateValueType
selected: boolean
}
export interface TemplateQuery {
command: string
db: string
database?: string
db?: string
rp?: string
measurement: string
tagKey: string
fieldKey: string
influxql: string
measurement?: string
tagKey?: string
fieldKey?: string
influxql?: string
}
export enum TemplateType {
AutoGroupBy = 'autoGroupBy',
Constant = 'constant',
FieldKeys = 'fieldKeys',
Measurements = 'measurements',
TagKeys = 'tagKeys',
TagValues = 'tagValues',
CSV = 'csv',
Query = 'query',
Databases = 'databases',
}
export interface Template {
id: string
tempVar: string
values: TemplateValue[]
type: string
type: TemplateType
label: string
query?: TemplateQuery
}
@ -32,3 +55,9 @@ export interface TemplateUpdate {
export interface URLQueryParams {
[key: string]: string
}
export interface TemplateBuilderProps {
template: Template
source: Source
onUpdateTemplate: (nextTemplate: Template) => void
}

View File

@ -3,6 +3,7 @@ import _ from 'lodash'
import reducer from 'src/dashboards/reducers/ui'
import {template, dashboard, cell} from 'test/resources'
import {initialState} from 'src/dashboards/reducers/ui'
import {TemplateType, TemplateValueType} from 'src/types'
import {
setTimeRange,
@ -21,13 +22,13 @@ let state
const t2 = {
...template,
id: '2',
type: 'csv',
type: TemplateType.CSV,
label: 'test csv',
tempVar: ':temperature:',
values: [
{value: '98.7', type: 'measurement', selected: false},
{value: '99.1', type: 'measurement', selected: false},
{value: '101.3', type: 'measurement', selected: true},
{value: '98.7', type: TemplateValueType.Measurement, selected: false},
{value: '99.1', type: TemplateValueType.Measurement, selected: false},
{value: '101.3', type: TemplateValueType.Measurement, selected: true},
],
}

View File

@ -1,4 +1,4 @@
import {applyMasks, insertTempVar, unMask} from 'src/dashboards/constants'
import {applyMasks, insertTempVar, unMask} from 'src/tempVars/constants'
const masquerade = query => {
const masked = applyMasks(query)

View File

@ -7,6 +7,8 @@ import {
TimeRange,
Template,
QueryConfig,
TemplateType,
TemplateValueType,
} from 'src/types'
import {
Axes,
@ -201,57 +203,57 @@ export const timeRange: TimeRange = {
export const userDefinedTemplateVariables: Template[] = [
{
tempVar: ':fields:',
type: 'fieldKeys',
type: TemplateType.FieldKeys,
label: '',
values: [
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_guest',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_guest_nice',
},
{
selected: true,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_idle',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_iowait',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_irq',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_nice',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_softirq',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_steal',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_system',
},
{
selected: false,
type: 'fieldKey',
type: TemplateValueType.FieldKey,
value: 'usage_user',
},
],
@ -259,42 +261,42 @@ export const userDefinedTemplateVariables: Template[] = [
},
{
tempVar: ':measurements:',
type: 'measurements',
type: TemplateType.Measurements,
label: '',
values: [
{
selected: true,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'cpu',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'disk',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'diskio',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'mem',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'processes',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'swap',
},
{
selected: false,
type: 'measurement',
type: TemplateValueType.Measurement,
value: 'system',
},
],
@ -305,11 +307,11 @@ export const userDefinedTemplateVariables: Template[] = [
const dashtimeTempVar: Template = {
id: 'dashtime',
tempVar: ':dashboardTime:',
type: 'constant',
type: TemplateType.Constant,
values: [
{
value: 'now() - 5m',
type: 'constant',
type: TemplateValueType.Constant,
selected: true,
},
],
@ -318,11 +320,11 @@ const dashtimeTempVar: Template = {
const upperdashtimeTempVar: Template = {
id: 'upperdashtime',
tempVar: ':upperDashboardTime:',
type: 'constant',
type: TemplateType.Constant,
values: [
{
value: 'now()',
type: 'constant',
type: TemplateValueType.Constant,
selected: true,
},
],

View File

@ -1,5 +1,5 @@
import {Source, Template, Dashboard, Cell, CellType} from 'src/types'
import {SourceLinks} from 'src/types/sources'
import {SourceLinks, TemplateType, TemplateValueType} from 'src/types'
export const role = {
name: '',
@ -590,12 +590,11 @@ export const hosts = {
// Dashboards
export const template: Template = {
id: '1',
type: 'tagKeys',
type: TemplateType.TagKeys,
label: 'test query',
tempVar: ':region:',
query: {
db: 'db1',
command: '',
rp: 'rp1',
tagKey: 'tk1',
fieldKey: 'fk1',
@ -603,9 +602,9 @@ export const template: Template = {
influxql: 'SHOW TAGS WHERE CHRONOGIRAFFE = "friend"',
},
values: [
{value: 'us-west', type: 'tagKey', selected: false},
{value: 'us-east', type: 'tagKey', selected: true},
{value: 'us-mount', type: 'tagKey', selected: false},
{value: 'us-west', type: TemplateValueType.TagKey, selected: false},
{value: 'us-east', type: TemplateValueType.TagKey, selected: true},
{value: 'us-mount', type: TemplateValueType.TagKey, selected: false},
],
}

View File

@ -1,8 +1,10 @@
import React from 'react'
import {shallow} from 'enzyme'
import TemplateControlBar from 'src/dashboards/components/TemplateControlBar'
import TemplateControlDropdown from 'src/dashboards/components/TemplateControlDropdown'
import TemplateControlBar from 'src/tempVars/components/TemplateControlBar'
import TemplateControlDropdown from 'src/tempVars/components/TemplateControlDropdown'
import {TemplateType, TemplateValueType} from 'src/types'
import {source} from 'test/resources'
const defaultProps = {
isOpen: true,
@ -11,16 +13,16 @@ const defaultProps = {
id: '000',
tempVar: ':alpha:',
label: '',
type: 'constant',
type: TemplateType.Constant,
values: [
{
value: 'firstValue',
type: 'constant',
type: TemplateValueType.Constant,
selected: false,
},
{
value: 'secondValue',
type: 'constant',
type: TemplateValueType.Constant,
selected: false,
},
],
@ -30,6 +32,9 @@ const defaultProps = {
isUsingAuth: true,
onOpenTemplateManager: () => {},
onSelectTemplate: () => {},
onSaveTemplates: () => {},
onCreateTemplateVariable: () => {},
source,
}
const setup = (override = {}) => {