Merge pull request #3659 from influxdata/code-mirror-data-explorer

Upgrade Data Explorer Query Field with CodeMirror2
pull/10616/head
Alex Paxton 2018-06-15 08:48:25 -07:00 committed by GitHub
commit 493ffb264a
7 changed files with 221 additions and 70 deletions

View File

@ -10,6 +10,7 @@
1. [#3474](https://github.com/influxdata/chronograf/pull/3474): Sort task table on Manage Alert page alphabetically
1. [#3590](https://github.com/influxdata/chronograf/pull/3590): Redesign icons in side navigation
1. [#3659](https://github.com/influxdata/chronograf/pull/3659): Upgrade Data Explorer query text field with syntax highlighting and partial multi-line support
1. [#3663](https://github.com/influxdata/chronograf/pull/3663): Truncate message preview in Alert Rules table
### Bug Fixes

View File

@ -1,10 +1,14 @@
import React, {PureComponent, KeyboardEvent} from 'react'
import React, {PureComponent} from 'react'
import {Controlled as ReactCodeMirror, IInstance} from 'react-codemirror2'
import {EditorChange} from 'codemirror'
import Dropdown from 'src/shared/components/Dropdown'
import classnames from 'classnames'
import {QUERY_TEMPLATES, QueryTemplate} from 'src/data_explorer/constants'
import QueryStatus from 'src/shared/components/QueryStatus'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {QueryConfig} from 'src/types'
import 'src/external/codemirror'
interface Props {
query: string
@ -14,19 +18,17 @@ interface Props {
interface State {
value: string
focused: boolean
}
@ErrorHandling
class QueryEditor extends PureComponent<Props, State> {
private editor: React.RefObject<HTMLTextAreaElement>
constructor(props) {
super(props)
this.state = {
value: this.props.query,
focused: false,
}
this.editor = React.createRef<HTMLTextAreaElement>()
}
public componentWillReceiveProps(nextProps: Props) {
@ -41,21 +43,34 @@ class QueryEditor extends PureComponent<Props, State> {
} = this.props
const {value} = this.state
const options = {
tabIndex: 1,
mode: 'influxQL',
readonly: false,
lineNumbers: false,
autoRefresh: true,
theme: 'influxql',
completeSingle: false,
lineWrapping: true,
}
return (
<div className="query-editor">
<textarea
className="query-editor--field"
ref={this.editor}
value={value}
autoComplete="off"
spellCheck={false}
onBlur={this.handleUpdate}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
data-test="query-editor-field"
placeholder="Enter a query or select database, measurement, and field below and have us build one for you..."
/>
<div className="varmoji">
<div className={this.queryCodeClassName}>
<ReactCodeMirror
autoFocus={true}
autoCursor={true}
value={value}
options={options}
onBeforeChange={this.updateCode}
onChange={this.handleChange}
onTouchStart={this.onTouchStart}
onBlur={this.handleBlur}
onFocus={this.handleFocus}
onKeyUp={this.handleKeyUp}
/>
</div>
<div className={this.varmojiClassName}>
<div className="varmoji-container">
<div className="varmoji-front">
<QueryStatus status={status}>
@ -66,6 +81,12 @@ class QueryEditor extends PureComponent<Props, State> {
className="dropdown-140 query-editor--templates"
buttonSize="btn-xs"
/>
<button
className="btn btn-xs btn-primary query-editor--submit"
onClick={this.handleSubmit}
>
Submit Query
</button>
</QueryStatus>
</div>
</div>
@ -74,32 +95,54 @@ class QueryEditor extends PureComponent<Props, State> {
)
}
private handleKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>): void => {
const {value} = this.state
if (e.key === 'Escape') {
e.preventDefault()
this.setState({value})
}
if (e.key === 'Enter') {
e.preventDefault()
this.handleUpdate()
}
}
private handleChange = (): void => {
const value = this.editor.current.value
this.setState({value})
}
private handleUpdate = (): void => {
private handleSubmit = (): void => {
this.props.onUpdate(this.state.value)
}
private get queryCodeClassName(): string {
const {focused} = this.state
return classnames('query-editor--code', {focus: focused})
}
private get varmojiClassName(): string {
const {focused} = this.state
return classnames('varmoji', {focus: focused})
}
private onTouchStart = () => {}
private handleChange = (): void => {}
private handleBlur = (): void => {
this.setState({focused: false})
this.handleSubmit()
}
private handleFocus = (): void => {
this.setState({focused: true})
}
private handleChooseMetaQuery = (template: QueryTemplate): void => {
this.setState({value: template.query})
}
private handleKeyUp = (__, e: KeyboardEvent) => {
const {ctrlKey, metaKey, key} = e
if (key === 'Enter' && (ctrlKey || metaKey)) {
this.handleSubmit()
}
}
private updateCode = (
_: IInstance,
__: EditorChange,
value: string
): void => {
this.setState({value})
}
}
export default QueryEditor

View File

@ -1,4 +1,8 @@
import {modeFlux, modeTickscript} from 'src/shared/constants/codeMirrorModes'
import {
modeFlux,
modeTickscript,
modeInfluxQL,
} from 'src/shared/constants/codeMirrorModes'
import 'codemirror/addon/hint/show-hint'
/* eslint-disable */
@ -314,4 +318,5 @@ function indentFunction(states, meta) {
// Modes
CodeMirror.defineSimpleMode('flux', modeFlux)
CodeMirror.defineSimpleMode('tickscript', modeTickscript)
CodeMirror.defineSimpleMode('tickscript', modeTickscript)
CodeMirror.defineSimpleMode('influxQL', modeInfluxQL)

View File

@ -174,3 +174,80 @@ export const modeTickscript = {
lineComment: '//',
},
}
export const modeInfluxQL = {
// The start state contains the rules that are intially used
start: [
// The regex matches the token, the token property contains the type
{
regex: /"(?:[^\\]|\\.)*?(?:"|$)/,
token: 'string.double',
},
{
regex: /'(?:[^\\]|\\.)*?(?:'|$)/,
token: 'string.single',
},
{
regex: /(function)(\s+)([a-z$][\w$]*)/,
token: ['keyword', null, 'variable-2'],
},
// Rules are matched in the order in which they appear, so there is
// no ambiguity between this one and the one above
{
regex: /(SELECT\s|AS\s|FROM\s|WHERE\s|GROUP\sBY\s)/i,
token: 'clause',
},
{
regex: /FILL(?=[(])/i,
token: 'clause',
},
{
regex: /(CREATE\s|SHOW\s|DROP\s)/i,
token: 'meta',
},
{
regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,
token: 'number',
},
{
regex: /[:](interval|dashboardTime|dashboardUpper)[:]/,
token: 'temp-system',
},
{
regex: /[:]\w+[:]/,
token: 'temp-var',
},
{
regex: /now[(][)]\s\S\s\S+/,
token: 'now',
},
{
regex: /[-+\/*=~<>!]+/,
token: 'operator',
},
{
regex: /(NULL)/i,
token: 'null',
},
],
// The multi-line comment state.
comment: [
{
regex: /.*?\*\//,
token: 'comment',
next: 'start',
},
{
regex: /.*/,
token: 'comment',
},
],
// The meta property contains global information about the mode. It
// can contain properties like lineComment, which are supported by
// all modes, and also directives like dontIndentStates, which are
// specific to simple modes.
meta: {
dontIndentStates: ['comment'],
lineComment: '//',
},
}

View File

@ -0,0 +1,42 @@
/*
CodeMirror "InfluxQL" Theme
------------------------------------------------------------------------------
Intended for use with the influxQL CodeMirror Mode
*/
.cm-s-influxql {
color: $g11-sidewalk;
font-weight: 600;
.cm-clause {
color: $c-pool;
}
.cm-meta {
color: $c-honeydew;
}
.cm-string {
color: $g15-platinum;
}
.cm-comment {
color: $g8-storm;
}
.cm-number {
color: $c-pineapple;
}
.cm-operator {
color: $c-pool;
}
.cm-temp-var {
color: $c-comet;
}
.cm-temp-system {
color: #ff4d9e ;
}
.cm-now {
color: $c-pineapple;
}
.cm-null {
color: $g8-storm;
font-style: italic;
}
}

View File

@ -90,4 +90,5 @@ div.CodeMirror-selected,
*/
@import 'time-machine';
@import 'tickscript';
@import 'influxql';
@import 'hints';

View File

@ -13,40 +13,15 @@
position: relative;
z-index: 3; /* 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;
word-wrap: break-word;
word-break: break-all;
@include custom-scrollbar($query-editor--field-bg, $query-editor--field-text);
display: block;
resize: none;
width: 100%;
height: $query-editor--field-height;
transition:
color 0.25s ease,
background-color 0.25s ease,
border-color 0.25s ease;
.query-editor--code {
background-color: $query-editor--field-bg;
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;
margin: 0;
padding: 6px 8px;
transition: border-color 0.25s ease;
&:hover,
&:hover + .query-editor--status {
border-color: $query-editor--bg;
}
&:focus {
outline: none;
color: $query-editor--field-text !important;
border-color: $c-pool;
}
&:focus + .varmoji {
&.focus {
border-color: $c-pool;
}
}
@ -96,6 +71,9 @@
max-width: $query-editor--templates-menu-width;
}
}
button.btn.query-editor--submit {
margin-right: 4px;
}
/*
@ -113,6 +91,10 @@
height: $query-editor--status-height;
width: 100%;
perspective: 1000px;
&.focus {
border-color: $c-pool;
}
}
.varmoji-container {
transition: transform 0.6s ease;