Merge pull request #1326 from influxdata/feature/template-varmojis
Feature/template varmojispull/1304/head
commit
7db2d74ad3
|
@ -676,7 +676,7 @@
|
|||
* node-libs-browser 0.6.0 [MIT](https://github.com/webpack/node-libs-browser)
|
||||
* node-notifier 4.6.1 [MIT](ssh://git@github.com/mikaelbr/node-notifier)
|
||||
* node-pre-gyp 0.6.29 [BSD-3-Clause](http://github.com/mapbox/node-pre-gyp)
|
||||
* node-sass 3.11.3 [MIT](https://github.com/sass/node-sass)
|
||||
* node-sass 4.5.2 [MIT](https://github.com/sass/node-sass)
|
||||
* node-uuid 1.4.7 [MIT](https://github.com/broofa/node-uuid)
|
||||
* nopt 3.0.6 [ISC](https://github.com/npm/nopt)
|
||||
* normalize-package-data 2.3.5 [BSD;BSD-2-Clause](http://github.com/npm/normalize-package-data)
|
||||
|
|
|
@ -149,7 +149,7 @@
|
|||
'eol-last': 0, // TODO: revisit
|
||||
'id-length': 0,
|
||||
'id-match': 0,
|
||||
'indent': [2, 2, {SwitchCase: 1}],
|
||||
'indent': [0, 2, {SwitchCase: 1}],
|
||||
'key-spacing': [2, {beforeColon: false, afterColon: true}],
|
||||
'linebreak-style': [2, 'unix'],
|
||||
'lines-around-comment': 0,
|
||||
|
|
|
@ -70,7 +70,7 @@
|
|||
"mocha": "^2.4.5",
|
||||
"mocha-loader": "^0.7.1",
|
||||
"mustache": "^2.2.1",
|
||||
"node-sass": "^3.5.3",
|
||||
"node-sass": "^4.5.2",
|
||||
"postcss-browser-reporter": "^0.4.0",
|
||||
"postcss-calc": "^5.2.0",
|
||||
"postcss-loader": "^0.8.0",
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import {TEMPLATE_MATCHER} from 'src/dashboards/constants'
|
||||
|
||||
describe('templating', () => {
|
||||
describe('matching', () => {
|
||||
it('can match the expected strings', () => {
|
||||
const matchingStrings = [
|
||||
'SELECT : FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||
'SELECT :t, "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||
'SELECT :tv1, "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||
'SELECT "f1" FROM "db1"."rp1"."m1" WHERE time > now() - :tv',
|
||||
]
|
||||
|
||||
matchingStrings.forEach(s => {
|
||||
const result = s.match(TEMPLATE_MATCHER)
|
||||
expect(result.length).to.be.above(0)
|
||||
})
|
||||
})
|
||||
|
||||
it('does not match unexpected strings', () => {
|
||||
const nonMatchingStrings = [
|
||||
'SELECT "foo", "f1" FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||
'SELECT :tv1:, :tv2: FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||
]
|
||||
|
||||
nonMatchingStrings.forEach(s => {
|
||||
const result = s.match(TEMPLATE_MATCHER)
|
||||
expect(result).to.equal(null)
|
||||
})
|
||||
})
|
||||
|
||||
it('only matches when starts with : but does not end in :', () => {
|
||||
const matchingStrings = [
|
||||
'SELECT :tv1, :tv2: FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||
'SELECT :tv1:, :tv2 FROM "db1"."rp1"."m1" WHERE time > now() - 15m',
|
||||
]
|
||||
|
||||
matchingStrings.forEach(s => {
|
||||
const result = s.match(TEMPLATE_MATCHER)
|
||||
expect(result.length).to.equal(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('replacing', () => {
|
||||
const tempVar = ':tv1:'
|
||||
it('can replace the expected strings', () => {
|
||||
const s = 'SELECT :fasdf FROM "db1"."rp1"."m1"'
|
||||
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||
const expected = `SELECT ${tempVar} FROM "db1"."rp1"."m1"`
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
|
||||
it('can replace a string with a numeric character', () => {
|
||||
const s = 'SELECT :fas0df FROM "db1"."rp1"."m1"'
|
||||
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||
const expected = `SELECT ${tempVar} FROM "db1"."rp1"."m1"`
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
|
||||
it('can replace the expected strings that are next to ,', () => {
|
||||
const s = 'SELECT :fasdf, "f1" FROM "db1"."rp1"."m1"'
|
||||
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||
const expected = `SELECT ${tempVar}, "f1" FROM "db1"."rp1"."m1"`
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
|
||||
it('can replace the expected strings that are next to .', () => {
|
||||
const s = 'SELECT "f1" FROM "db1".:asdf."m1"'
|
||||
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||
const expected = `SELECT "f1" FROM "db1".${tempVar}."m1"`
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
|
||||
it('can does not replace other tempVars', () => {
|
||||
const s = 'SELECT :foo: FROM "db1".:asdfasd."m1"'
|
||||
const actual = s.replace(TEMPLATE_MATCHER, tempVar)
|
||||
const expected = `SELECT :foo: FROM "db1".${tempVar}."m1"`
|
||||
|
||||
expect(actual).to.equal(expected)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -28,7 +28,6 @@ const CheckSources = React.createClass({
|
|||
}).isRequired,
|
||||
})
|
||||
),
|
||||
addFlashMessage: func,
|
||||
children: node,
|
||||
params: shape({
|
||||
sourceID: string,
|
||||
|
|
|
@ -131,6 +131,7 @@ class CellEditorOverlay extends Component {
|
|||
const {
|
||||
source,
|
||||
onCancel,
|
||||
templates,
|
||||
timeRange,
|
||||
autoRefresh,
|
||||
editQueryStatus,
|
||||
|
@ -174,6 +175,7 @@ class CellEditorOverlay extends Component {
|
|||
/>
|
||||
<QueryMaker
|
||||
source={source}
|
||||
templates={templates}
|
||||
queries={queriesWorkingDraft}
|
||||
actions={queryActions}
|
||||
autoRefresh={autoRefresh}
|
||||
|
@ -190,12 +192,17 @@ class CellEditorOverlay extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const {func, number, shape, string} = PropTypes
|
||||
const {arrayOf, func, number, shape, string} = PropTypes
|
||||
|
||||
CellEditorOverlay.propTypes = {
|
||||
onCancel: func.isRequired,
|
||||
onSave: func.isRequired,
|
||||
cell: shape({}).isRequired,
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
tempVar: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
timeRange: shape({
|
||||
upper: string,
|
||||
lower: string,
|
||||
|
|
|
@ -70,3 +70,5 @@ export const TEMPLATE_VARIABLE_QUERIES = {
|
|||
tagKeys: 'SHOW TAG KEYS ON :database: FROM :measurement:',
|
||||
tagValues: 'SHOW TAG VALUES ON :database: FROM :measurement: WITH KEY=:tagKey:',
|
||||
}
|
||||
|
||||
export const TEMPLATE_MATCHER = /\B:\B|:\w+\b(?!:)/g
|
||||
|
|
|
@ -244,6 +244,7 @@ class DashboardPage extends Component {
|
|||
{selectedCell
|
||||
? <CellEditorOverlay
|
||||
source={source}
|
||||
templates={dashboard.templates}
|
||||
cell={selectedCell}
|
||||
timeRange={timeRange}
|
||||
autoRefresh={autoRefresh}
|
||||
|
@ -348,11 +349,11 @@ DashboardPage.propTypes = {
|
|||
}),
|
||||
values: arrayOf(
|
||||
shape({
|
||||
type: string.isRequired,
|
||||
value: string.isRequired,
|
||||
selected: bool,
|
||||
selected: bool.isRequired,
|
||||
type: string.isRequired,
|
||||
})
|
||||
).isRequired,
|
||||
),
|
||||
})
|
||||
),
|
||||
})
|
||||
|
|
|
@ -7,11 +7,7 @@ import TagList from './TagList'
|
|||
import QueryEditor from './QueryEditor'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
|
||||
const {
|
||||
string,
|
||||
shape,
|
||||
func,
|
||||
} = PropTypes
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
const QueryBuilder = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -27,6 +23,11 @@ const QueryBuilder = React.createClass({
|
|||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
tempVar: string.isRequired,
|
||||
})
|
||||
),
|
||||
actions: shape({
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
|
@ -78,12 +79,12 @@ const QueryBuilder = React.createClass({
|
|||
},
|
||||
|
||||
render() {
|
||||
const {query, timeRange} = this.props
|
||||
const {query, timeRange, templates} = this.props
|
||||
const q = query.rawText || buildInfluxQLQuery(timeRange, query) || ''
|
||||
|
||||
return (
|
||||
<div className="query-maker--tab-contents">
|
||||
<QueryEditor query={q} config={query} onUpdate={this.handleEditRawText} />
|
||||
<QueryEditor query={q} config={query} onUpdate={this.handleEditRawText} templates={templates} />
|
||||
{this.renderLists()}
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,66 +1,195 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import React, {PropTypes, Component} from 'react'
|
||||
import _ from 'lodash'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import Dropdown from 'src/shared/components/Dropdown'
|
||||
import LoadingDots from 'src/shared/components/LoadingDots'
|
||||
import TemplateDrawer from 'src/shared/components/TemplateDrawer'
|
||||
import {QUERY_TEMPLATES} from 'src/data_explorer/constants'
|
||||
import {TEMPLATE_MATCHER} from 'src/dashboards/constants'
|
||||
|
||||
const ENTER = 13
|
||||
const ESCAPE = 27
|
||||
const {bool, func, shape, string} = PropTypes
|
||||
const QueryEditor = React.createClass({
|
||||
propTypes: {
|
||||
query: string.isRequired,
|
||||
onUpdate: func.isRequired,
|
||||
config: shape({
|
||||
status: shape({
|
||||
error: string,
|
||||
loading: bool,
|
||||
success: string,
|
||||
warn: string,
|
||||
}),
|
||||
}).isRequired,
|
||||
},
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
class QueryEditor extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
value: this.props.query,
|
||||
isTemplating: false,
|
||||
selectedTemplate: {
|
||||
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
|
||||
},
|
||||
filteredTemplates: this.props.templates,
|
||||
}
|
||||
},
|
||||
|
||||
this.handleKeyDown = ::this.handleKeyDown
|
||||
this.handleChange = ::this.handleChange
|
||||
this.handleUpdate = ::this.handleUpdate
|
||||
this.handleChooseTemplate = ::this.handleChooseTemplate
|
||||
this.handleCloseDrawer = ::this.handleCloseDrawer
|
||||
this.findTempVar = ::this.findTempVar
|
||||
this.handleTemplateReplace = ::this.handleTemplateReplace
|
||||
this.handleMouseOverTempVar = ::this.handleMouseOverTempVar
|
||||
this.handleClickTempVar = ::this.handleClickTempVar
|
||||
this.closeDrawer = ::this.closeDrawer
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.query !== nextProps.query) {
|
||||
this.setState({value: nextProps.query})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleCloseDrawer() {
|
||||
this.setState({isTemplating: false})
|
||||
}
|
||||
|
||||
handleMouseOverTempVar(template) {
|
||||
this.handleTemplateReplace(template)
|
||||
}
|
||||
|
||||
handleClickTempVar(template) {
|
||||
// Clicking a tempVar does the same thing as hitting 'Enter'
|
||||
this.handleTemplateReplace(template, 'Enter')
|
||||
this.closeDrawer()
|
||||
}
|
||||
|
||||
closeDrawer() {
|
||||
this.setState({
|
||||
isTemplating: false,
|
||||
selectedTemplate: {
|
||||
tempVar: _.get(this.props.templates, ['0', 'tempVar'], ''),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.keyCode === ENTER) {
|
||||
const {isTemplating, value} = this.state
|
||||
|
||||
if (isTemplating) {
|
||||
switch (e.key) {
|
||||
case 'ArrowRight':
|
||||
case 'ArrowDown':
|
||||
e.preventDefault()
|
||||
return this.handleTemplateReplace(this.findTempVar('next'))
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowUp':
|
||||
e.preventDefault()
|
||||
return this.handleTemplateReplace(this.findTempVar('previous'))
|
||||
case 'Enter':
|
||||
e.preventDefault()
|
||||
this.handleTemplateReplace(this.state.selectedTemplate, e.key)
|
||||
return this.closeDrawer()
|
||||
case 'Escape':
|
||||
e.preventDefault()
|
||||
return this.closeDrawer()
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
this.setState({value, isTemplating: false})
|
||||
} else if (e.key === 'Enter') {
|
||||
e.preventDefault()
|
||||
this.handleUpdate()
|
||||
} else if (e.keyCode === ESCAPE) {
|
||||
this.setState({value: this.state.value}, () => {
|
||||
this.editor.blur()
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
handleTemplateReplace(selectedTemplate, key) {
|
||||
const {selectionStart, value} = this.editor
|
||||
const isEnter = key === 'Enter'
|
||||
const {tempVar} = selectedTemplate
|
||||
|
||||
let templatedValue
|
||||
const matched = value.match(TEMPLATE_MATCHER)
|
||||
if (matched) {
|
||||
const newTempVar = isEnter
|
||||
? tempVar
|
||||
: tempVar.substring(0, tempVar.length - 1)
|
||||
templatedValue = value.replace(TEMPLATE_MATCHER, newTempVar)
|
||||
}
|
||||
|
||||
const enterModifier = isEnter ? 0 : -1
|
||||
const diffInLength = tempVar.length - matched[0].length + enterModifier
|
||||
|
||||
this.setState({value: templatedValue, selectedTemplate}, () =>
|
||||
this.editor.setSelectionRange(
|
||||
selectionStart + diffInLength,
|
||||
selectionStart + diffInLength
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
findTempVar(direction) {
|
||||
const {filteredTemplates: templates} = this.state
|
||||
const {selectedTemplate} = this.state
|
||||
|
||||
const i = _.findIndex(templates, selectedTemplate)
|
||||
const lastIndex = templates.length - 1
|
||||
|
||||
if (i >= 0) {
|
||||
if (direction === 'next') {
|
||||
return templates[(i + 1) % templates.length]
|
||||
}
|
||||
|
||||
if (direction === 'previous') {
|
||||
if (i === 0) {
|
||||
return templates[lastIndex]
|
||||
}
|
||||
|
||||
return templates[i - 1]
|
||||
}
|
||||
}
|
||||
|
||||
return templates[0]
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
this.setState({
|
||||
value: this.editor.value,
|
||||
})
|
||||
},
|
||||
const {templates} = this.props
|
||||
const {selectedTemplate} = this.state
|
||||
const value = this.editor.value
|
||||
const matches = value.match(TEMPLATE_MATCHER)
|
||||
if (matches) {
|
||||
// maintain cursor poition
|
||||
const start = this.editor.selectionStart
|
||||
const end = this.editor.selectionEnd
|
||||
const filteredTemplates = templates.filter(t =>
|
||||
t.tempVar.includes(matches[0].substring(1))
|
||||
)
|
||||
|
||||
const found = filteredTemplates.find(
|
||||
t => t.tempVar === selectedTemplate && selectedTemplate.tempVar
|
||||
)
|
||||
const newTemplate = found ? found : filteredTemplates[0]
|
||||
|
||||
this.setState({
|
||||
isTemplating: true,
|
||||
selectedTemplate: newTemplate,
|
||||
filteredTemplates,
|
||||
value,
|
||||
})
|
||||
this.editor.setSelectionRange(start, end)
|
||||
} else {
|
||||
this.setState({isTemplating: false, value})
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdate() {
|
||||
this.props.onUpdate(this.state.value)
|
||||
},
|
||||
}
|
||||
|
||||
handleChooseTemplate(template) {
|
||||
this.setState({value: template.query})
|
||||
},
|
||||
}
|
||||
|
||||
handleSelectTempVar(tempVar) {
|
||||
this.setState({selectedTemplate: tempVar})
|
||||
}
|
||||
|
||||
render() {
|
||||
const {config: {status}} = this.props
|
||||
const {value} = this.state
|
||||
const {
|
||||
value,
|
||||
isTemplating,
|
||||
selectedTemplate,
|
||||
filteredTemplates,
|
||||
} = this.state
|
||||
|
||||
return (
|
||||
<div className="query-editor">
|
||||
|
@ -69,13 +198,80 @@ const QueryEditor = React.createClass({
|
|||
onChange={this.handleChange}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onBlur={this.handleUpdate}
|
||||
ref={editor => (this.editor = editor)}
|
||||
ref={editor => this.editor = editor}
|
||||
value={value}
|
||||
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
|
||||
autoComplete="off"
|
||||
spellCheck="false"
|
||||
/>
|
||||
{this.renderStatus(status)}
|
||||
<div
|
||||
className={classNames('varmoji', {'varmoji-rotated': isTemplating})}
|
||||
>
|
||||
<div className="varmoji-container">
|
||||
<div className="varmoji-front">{this.renderStatus(status)}</div>
|
||||
<div className="varmoji-back">
|
||||
{isTemplating
|
||||
? <TemplateDrawer
|
||||
onClickTempVar={this.handleClickTempVar}
|
||||
templates={filteredTemplates}
|
||||
selected={selectedTemplate}
|
||||
onMouseOverTempVar={this.handleMouseOverTempVar}
|
||||
handleClickOutside={this.handleCloseDrawer}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
renderStatus(status) {
|
||||
if (!status) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status.loading) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<LoadingDots />
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
onChoose={this.handleChooseTemplate}
|
||||
className="query-editor--templates"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<span
|
||||
className={classNames('query-status-output', {
|
||||
'query-status-output--error': status.error,
|
||||
'query-status-output--success': status.success,
|
||||
'query-status-output--warning': status.warn,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={classNames('icon', {
|
||||
stop: status.error,
|
||||
checkmark: status.success,
|
||||
'alert-triangle': status.warn,
|
||||
})}
|
||||
/>
|
||||
{status.error || status.warn || status.success}
|
||||
</span>
|
||||
<Dropdown
|
||||
items={QUERY_TEMPLATES}
|
||||
selected={'Query Templates'}
|
||||
|
@ -84,40 +280,20 @@ const QueryEditor = React.createClass({
|
|||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
renderStatus(status) {
|
||||
if (!status) {
|
||||
return <div className="query-editor--status" />
|
||||
}
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
if (status.loading) {
|
||||
return (
|
||||
<div className="query-editor--status">
|
||||
<LoadingDots />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames('query-editor--status', {
|
||||
'query-editor--error': status.error,
|
||||
'query-editor--success': status.success,
|
||||
'query-editor--warning': status.warn,
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={classNames('icon', {
|
||||
stop: status.error,
|
||||
checkmark: status.success,
|
||||
'alert-triangle': status.warn,
|
||||
})}
|
||||
/>
|
||||
{status.error || status.warn || status.success}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
QueryEditor.propTypes = {
|
||||
query: string.isRequired,
|
||||
onUpdate: func.isRequired,
|
||||
config: shape().isRequired,
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
tempVar: string.isRequired,
|
||||
})
|
||||
),
|
||||
}
|
||||
|
||||
export default QueryEditor
|
||||
|
|
|
@ -4,14 +4,7 @@ import QueryBuilder from './QueryBuilder'
|
|||
import QueryMakerTab from './QueryMakerTab'
|
||||
import buildInfluxQLQuery from 'utils/influxql'
|
||||
|
||||
const {
|
||||
arrayOf,
|
||||
func,
|
||||
node,
|
||||
number,
|
||||
shape,
|
||||
string,
|
||||
} = PropTypes
|
||||
const {arrayOf, func, node, number, shape, string} = PropTypes
|
||||
|
||||
const QueryMaker = React.createClass({
|
||||
propTypes: {
|
||||
|
@ -25,6 +18,11 @@ const QueryMaker = React.createClass({
|
|||
upper: string,
|
||||
lower: string,
|
||||
}).isRequired,
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
tempVar: string.isRequired,
|
||||
})
|
||||
),
|
||||
actions: shape({
|
||||
chooseNamespace: func.isRequired,
|
||||
chooseMeasurement: func.isRequired,
|
||||
|
@ -76,7 +74,7 @@ const QueryMaker = React.createClass({
|
|||
},
|
||||
|
||||
renderQueryBuilder() {
|
||||
const {timeRange, actions, source} = this.props
|
||||
const {timeRange, actions, source, templates} = this.props
|
||||
const query = this.getActiveQuery()
|
||||
|
||||
if (!query) {
|
||||
|
@ -93,6 +91,7 @@ const QueryMaker = React.createClass({
|
|||
<QueryBuilder
|
||||
source={source}
|
||||
timeRange={timeRange}
|
||||
templates={templates}
|
||||
query={query}
|
||||
actions={actions}
|
||||
onAddQuery={this.handleAddQuery}
|
||||
|
|
|
@ -95,8 +95,8 @@ const AutoRefresh = ComposedComponent => {
|
|||
)
|
||||
},
|
||||
|
||||
async executeQueries(queries) {
|
||||
const {templates, editQueryStatus} = this.props
|
||||
executeQueries(queries) {
|
||||
const {templates = [], editQueryStatus} = this.props
|
||||
|
||||
if (!queries.length) {
|
||||
this.setState({timeSeries: []})
|
||||
|
|
|
@ -36,9 +36,6 @@ class Dropdown extends Component {
|
|||
if (e) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
if (this.props.onClick) {
|
||||
this.props.onClick(e)
|
||||
}
|
||||
this.setState({isOpen: !this.state.isOpen})
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import React, {PropTypes} from 'react'
|
||||
import OnClickOutside from 'react-onclickoutside'
|
||||
import classNames from 'classnames'
|
||||
|
||||
const TemplateDrawer = ({
|
||||
templates,
|
||||
selected,
|
||||
onMouseOverTempVar,
|
||||
onClickTempVar,
|
||||
}) => (
|
||||
<div className="template-drawer">
|
||||
{templates.map(t => (
|
||||
<div className={classNames('template-drawer--item', {'template-drawer--selected': t.tempVar === selected.tempVar})}
|
||||
onMouseOver={() => {
|
||||
onMouseOverTempVar(t)
|
||||
}}
|
||||
onClick={() => onClickTempVar(t)}
|
||||
key={t.tempVar}
|
||||
>
|
||||
{' '}{t.tempVar}{' '}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
const {arrayOf, func, shape, string} = PropTypes
|
||||
|
||||
TemplateDrawer.propTypes = {
|
||||
templates: arrayOf(
|
||||
shape({
|
||||
tempVar: string.isRequired,
|
||||
})
|
||||
),
|
||||
selected: shape({
|
||||
tempVar: string,
|
||||
}),
|
||||
onMouseOverTempVar: func.isRequired,
|
||||
onClickTempVar: func.isRequired,
|
||||
}
|
||||
|
||||
export default OnClickOutside(TemplateDrawer)
|
|
@ -11,19 +11,10 @@
|
|||
border-radius: 0 $radius 0 0;
|
||||
background-color: $query-editor--bg;
|
||||
position: relative;
|
||||
}
|
||||
.query-editor--field,
|
||||
.query-editor--status {
|
||||
font-family: $code-font;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
border: 2px solid $query-editor--bg;
|
||||
background-color: $query-editor--field-bg;
|
||||
|
||||
z-index: 2; /* Minimum amount to obcure the toggle flip within Query Builder. Will fix later */
|
||||
}
|
||||
.query-editor--field {
|
||||
font-family: $code-font;
|
||||
font-size: 12px;
|
||||
line-height: 14px;
|
||||
font-weight: 600;
|
||||
|
@ -34,7 +25,13 @@
|
|||
resize: none;
|
||||
width: 100%;
|
||||
height: $query-editor--field-height;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease,
|
||||
border-color 0.25s ease;
|
||||
border: 2px solid $query-editor--bg;
|
||||
border-bottom: 0;
|
||||
background-color: $query-editor--field-bg;
|
||||
color: $query-editor--field-text;
|
||||
padding: 12px 10px 0 10px;
|
||||
border-radius: $radius $radius 0 0;
|
||||
|
@ -49,33 +46,16 @@
|
|||
color: $query-editor--field-text !important;
|
||||
border-color: $c-pool;
|
||||
}
|
||||
&:focus + .query-editor--status {
|
||||
&:focus + .varmoji {
|
||||
border-color: $c-pool;
|
||||
}
|
||||
}
|
||||
.query-editor--status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
height: $query-editor--status-height;
|
||||
line-height: $query-editor--status-height;
|
||||
font-size: 12px;
|
||||
padding: 0 10px;
|
||||
padding-right: ($query-editor--templates-width + ($query-editor--templates-offset * 2)) !important;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
border-top: 0;
|
||||
color: $query-editor--status-default;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
span.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
/* Error State */
|
||||
&.query-editor--error { color: $query-editor--status-error; }
|
||||
/* Warning State */
|
||||
&.query-editor--warning { color: $query-editor--status-warning; }
|
||||
/* Success State */
|
||||
&.query-editor--success { color: $query-editor--status-success; }
|
||||
/* Loading State */
|
||||
.loading-dots {
|
||||
bottom: $query-editor--templates-offset;
|
||||
|
@ -83,10 +63,30 @@
|
|||
transform: translateY(50%);
|
||||
}
|
||||
}
|
||||
.query-status-output {
|
||||
flex: 1 0 0;
|
||||
display: inline-block;
|
||||
color: $query-editor--status-default;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 10px;
|
||||
line-height: $query-editor--status-height;
|
||||
font-size: 12px;
|
||||
font-family: $code-font;
|
||||
|
||||
span.icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
/* Error State */
|
||||
&.query-status-output--error { color: $query-editor--status-error; }
|
||||
/* Warning State */
|
||||
&.query-status-output--warning { color: $query-editor--status-warning; }
|
||||
/* Success State */
|
||||
&.query-status-output--success { color: $query-editor--status-success; }
|
||||
}
|
||||
.dropdown.query-editor--templates {
|
||||
position: absolute;
|
||||
bottom: ($query-editor--templates-offset - 8px);
|
||||
right: $query-editor--templates-offset;
|
||||
margin: 0 4px 0 0 ;
|
||||
|
||||
div.dropdown-toggle.btn.btn-sm {
|
||||
width: $query-editor--templates-width;
|
||||
|
@ -103,4 +103,85 @@
|
|||
min-width: $query-editor--templates-menu-width;
|
||||
max-width: $query-editor--templates-menu-width;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
Varmoji Flipper
|
||||
-------------------------------------
|
||||
Handles the 3D flip transition between two states (isTemplating)
|
||||
Contents could in theory be anything
|
||||
*/
|
||||
.varmoji {
|
||||
transition: border-color 0.25s ease;
|
||||
border: 2px solid $query-editor--bg;
|
||||
border-top: 0;
|
||||
background-color: $query-editor--field-bg;
|
||||
border-radius: 0 0 $radius $radius;
|
||||
height: $query-editor--status-height;
|
||||
width: 100%;
|
||||
perspective: 1000px;
|
||||
}
|
||||
.varmoji-container {
|
||||
transition: transform 0.6s ease;
|
||||
transform-style: preserve-3d;
|
||||
position: relative;
|
||||
transform-origin: 100% #{$query-editor--status-height / 2};
|
||||
}
|
||||
.varmoji-front,
|
||||
.varmoji-back {
|
||||
backface-visibility: hidden;
|
||||
width: 100%;
|
||||
height: $query-editor--status-height;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
.varmoji-front {
|
||||
z-index: 3;
|
||||
transform: rotateX(0deg);
|
||||
}
|
||||
.varmoji-back {
|
||||
z-index: 2;
|
||||
transform: rotateX(180deg);
|
||||
}
|
||||
.varmoji.varmoji-rotated .varmoji-container {
|
||||
transform: rotateX(-180deg);
|
||||
}
|
||||
|
||||
/*
|
||||
Template Drawer
|
||||
-------------------------------------
|
||||
Not sure if this needs its own stylesheet
|
||||
*/
|
||||
|
||||
.template-drawer {
|
||||
height: $query-editor--status-height;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 4px;
|
||||
}
|
||||
.template-drawer--item {
|
||||
margin-right: 2px;
|
||||
display: inline-block;
|
||||
font-family: $code-font;
|
||||
font-weight: 700;
|
||||
font-size: 12px;
|
||||
height: ($query-editor--status-height - 14px);
|
||||
line-height: ($query-editor--status-height - 14px);
|
||||
padding: 0 6px;
|
||||
background-color: $query-editor--field-bg;
|
||||
color: $c-comet;
|
||||
border-radius: $radius-small;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
color 0.25s ease,
|
||||
background-color 0.25s ease;
|
||||
|
||||
/* Selected State */
|
||||
&.template-drawer--selected {
|
||||
color: $g20-white;
|
||||
background-color: $c-star;
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ $tvmp-table-gutter: 8px;
|
|||
|
||||
.template-variable-manager {
|
||||
max-width: $tvmp-panel-max-width;
|
||||
margin: 0 $tvmp-gutter;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.template-variable-manager--header {
|
||||
height: $chronograf-page-header-height;
|
||||
|
@ -220,4 +220,4 @@ $tvmp-table-gutter: 8px;
|
|||
.confirm-buttons + .btn-edit {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,8 @@ $overlay-z: 100;
|
|||
left: 0;
|
||||
right: 0;
|
||||
z-index: $overlay-z;
|
||||
|
||||
padding: 0 30px;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
display: block;
|
||||
|
@ -124,4 +125,4 @@ $overlay-z: 100;
|
|||
}
|
||||
.overlay-technology .query-maker--tab-contents {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
|
26
ui/yarn.lock
26
ui/yarn.lock
|
@ -4357,6 +4357,10 @@ lodash.merge@^4.4.0:
|
|||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5"
|
||||
|
||||
lodash.mergewith@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.0.tgz#150cf0a16791f5903b8891eab154609274bdea55"
|
||||
|
||||
lodash.pick@^4.2.0, lodash.pick@^4.2.1, lodash.pick@^4.4.0:
|
||||
version "4.4.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3"
|
||||
|
@ -4784,9 +4788,9 @@ node-pre-gyp@^0.6.29:
|
|||
tar "~2.2.1"
|
||||
tar-pack "~3.3.0"
|
||||
|
||||
node-sass@^3.5.3:
|
||||
version "3.13.1"
|
||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-3.13.1.tgz#7240fbbff2396304b4223527ed3020589c004fc2"
|
||||
node-sass@^4.5.2:
|
||||
version "4.5.2"
|
||||
resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.5.2.tgz#4012fa2bd129b1d6365117e88d9da0500d99da64"
|
||||
dependencies:
|
||||
async-foreach "^0.1.3"
|
||||
chalk "^1.1.1"
|
||||
|
@ -4797,13 +4801,15 @@ node-sass@^3.5.3:
|
|||
in-publish "^2.0.0"
|
||||
lodash.assign "^4.2.0"
|
||||
lodash.clonedeep "^4.3.2"
|
||||
lodash.mergewith "^4.6.0"
|
||||
meow "^3.7.0"
|
||||
mkdirp "^0.5.1"
|
||||
nan "^2.3.2"
|
||||
node-gyp "^3.3.1"
|
||||
npmlog "^4.0.0"
|
||||
request "^2.61.0"
|
||||
request "^2.79.0"
|
||||
sass-graph "^2.1.1"
|
||||
stdout-stream "^1.4.0"
|
||||
|
||||
node-uuid@^1.4.7:
|
||||
version "1.4.7"
|
||||
|
@ -6157,7 +6163,7 @@ request-progress@~2.0.1:
|
|||
dependencies:
|
||||
throttleit "^1.0.0"
|
||||
|
||||
request@2, request@^2.55.0, request@^2.61.0, request@^2.74.0, request@^2.79.0, request@~2.79.0:
|
||||
request@2, request@^2.55.0, request@^2.74.0, request@^2.79.0, request@~2.79.0:
|
||||
version "2.79.0"
|
||||
resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
|
||||
dependencies:
|
||||
|
@ -6325,11 +6331,11 @@ sell@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/sell/-/sell-1.0.0.tgz#3baca7e51f78ddee9e22eea1ac747a6368bd1630"
|
||||
|
||||
"semver@2 || 3 || 4 || 5", semver@^5.1.0, semver@~5.3.0:
|
||||
"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.1.0, semver@~5.3.0:
|
||||
version "5.3.0"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
|
||||
|
||||
"semver@2.x || 3.x || 4 || 5", semver@~4.3.3:
|
||||
semver@~4.3.3:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.6.tgz#300bc6e0e86374f7ba61068b5b1ecd57fc6532da"
|
||||
|
||||
|
@ -6676,6 +6682,12 @@ sshpk@^1.7.0:
|
|||
version "1.3.1"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e"
|
||||
|
||||
stdout-stream@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/stdout-stream/-/stdout-stream-1.4.0.tgz#a2c7c8587e54d9427ea9edb3ac3f2cd522df378b"
|
||||
dependencies:
|
||||
readable-stream "^2.0.1"
|
||||
|
||||
stream-browserify@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db"
|
||||
|
|
Loading…
Reference in New Issue