Merge pull request #3221 from influxdata/ifql/arg-inputs

Ifql/arg inputs
pull/10616/head
Andrew Watkins 2018-04-17 11:03:54 -07:00 committed by GitHub
commit 9f865efcc2
10 changed files with 353 additions and 9 deletions

View File

@ -0,0 +1,82 @@
import React, {PureComponent} from 'react'
import FuncArgInput, {OnChangeArg} from 'src/ifql/components/FuncArgInput'
import * as types from 'src/ifql/constants/argumentTypes'
interface Props {
funcID: string
argKey: string
value: string
type: string
onChangeArg: OnChangeArg
onGenerateScript: () => void
}
class FuncArg extends PureComponent<Props> {
public render() {
const {
argKey,
value,
type,
onChangeArg,
funcID,
onGenerateScript,
} = this.props
switch (type) {
case types.STRING:
case types.DURATION:
case types.TIME:
case types.REGEXP:
case types.FLOAT:
case types.INT:
case types.UINT:
case types.ARRAY: {
return (
<FuncArgInput
type={type}
value={value}
argKey={argKey}
funcID={funcID}
onChangeArg={onChangeArg}
onGenerateScript={onGenerateScript}
/>
)
}
case types.BOOL: {
// TODO: make boolean arg component
return (
<div className="func-arg">
{argKey} : {value}
</div>
)
}
case types.FUNCTION: {
// TODO: make separate function component
return (
<div className="func-arg">
{argKey} : {value}
</div>
)
}
case types.NIL: {
// TODO: handle nil type
return (
<div className="func-arg">
{argKey} : {value}
</div>
)
}
default: {
return (
<div className="func-arg">
{argKey} : {value}
</div>
)
}
}
}
}
export default FuncArg

View File

@ -0,0 +1,57 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
export type OnChangeArg = (inputArg: InputArg) => void
export interface InputArg {
funcID: string
key: string
value: string
}
interface Props {
funcID: string
argKey: string
value: string
type: string
onChangeArg: OnChangeArg
onGenerateScript: () => void
}
class FuncArgInput extends PureComponent<Props> {
public render() {
const {argKey, value, type} = this.props
return (
<div>
<label htmlFor={argKey}>{argKey}: </label>
<input
name={argKey}
value={value}
placeholder={type}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
type="text"
className="form-control input-xs"
spellCheck={false}
autoComplete="off"
/>
</div>
)
}
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter') {
return
}
this.props.onGenerateScript()
e.preventDefault()
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const {funcID, argKey} = this.props
this.props.onChangeArg({funcID, key: argKey, value: e.target.value})
}
}
export default FuncArgInput

View File

@ -1,4 +1,6 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import FuncArg from 'src/ifql/components/FuncArg'
import {OnChangeArg} from 'src/ifql/components/FuncArgInput'
interface Arg { interface Arg {
key: string key: string
@ -15,17 +17,29 @@ export interface Func {
interface Props { interface Props {
func: Func func: Func
onChangeArg: OnChangeArg
onGenerateScript: () => void
} }
export default class FuncArgs extends PureComponent<Props> { export default class FuncArgs extends PureComponent<Props> {
public render() { public render() {
const {func, onChangeArg, onGenerateScript} = this.props
return ( return (
<div className="func-args"> <div className="func-args">
{this.props.func.args.map(({key, value}) => ( {func.args.map(({key, value, type}) => {
<div className="func-arg" key={key}> return (
{key} : {value} <FuncArg
</div> funcID={func.id}
))} key={key}
type={type}
argKey={key}
value={value}
onChangeArg={onChangeArg}
onGenerateScript={onGenerateScript}
/>
)
})}
</div> </div>
) )
} }

View File

@ -1,10 +1,13 @@
import React, {PureComponent, MouseEvent} from 'react' import React, {PureComponent, MouseEvent} from 'react'
import FuncArgs from 'src/ifql/components/FuncArgs' import FuncArgs from 'src/ifql/components/FuncArgs'
import {Func} from 'src/ifql/components/FuncArgs' import {Func} from 'src/ifql/components/FuncArgs'
import {OnChangeArg} from 'src/ifql/components/FuncArgInput'
interface Props { interface Props {
func: Func func: Func
onDelete: (id: string) => void onDelete: (id: string) => void
onChangeArg: OnChangeArg
onGenerateScript: () => void
} }
interface State { interface State {
@ -20,7 +23,7 @@ export default class FuncNode extends PureComponent<Props, State> {
} }
public render() { public render() {
const {func} = this.props const {func, onChangeArg, onGenerateScript} = this.props
const {isOpen} = this.state const {isOpen} = this.state
return ( return (
@ -28,7 +31,13 @@ export default class FuncNode extends PureComponent<Props, State> {
<div className="func-node--name" onClick={this.handleClick}> <div className="func-node--name" onClick={this.handleClick}>
<div>{func.name}</div> <div>{func.name}</div>
</div> </div>
{isOpen && <FuncArgs func={func} />} {isOpen && (
<FuncArgs
func={func}
onChangeArg={onChangeArg}
onGenerateScript={onGenerateScript}
/>
)}
<div className="btn btn-danger btn-square" onClick={this.handleDelete}> <div className="btn btn-danger btn-square" onClick={this.handleDelete}>
<span className="icon-trash" /> <span className="icon-trash" />
</div> </div>
@ -40,7 +49,7 @@ export default class FuncNode extends PureComponent<Props, State> {
this.props.onDelete(this.props.func.id) this.props.onDelete(this.props.func.id)
} }
private handleClick = (e: MouseEvent<HTMLElement>) => { private handleClick = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation() e.stopPropagation()
const {isOpen} = this.state const {isOpen} = this.state

View File

@ -4,6 +4,7 @@ import FuncNode from 'src/ifql/components/FuncNode'
import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor' import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor'
import {Func} from 'src/ifql/components/FuncArgs' import {Func} from 'src/ifql/components/FuncArgs'
import {OnChangeArg} from 'src/ifql/components/FuncArgInput'
export interface Suggestion { export interface Suggestion {
name: string name: string
@ -20,6 +21,8 @@ interface Props {
onChangeScript: (script: string) => void onChangeScript: (script: string) => void
onSubmitScript: (script: string) => void onSubmitScript: (script: string) => void
onDeleteFuncNode: (id: string) => void onDeleteFuncNode: (id: string) => void
onChangeArg: OnChangeArg
onGenerateScript: () => void
} }
class TimeMachine extends PureComponent<Props> { class TimeMachine extends PureComponent<Props> {
@ -28,9 +31,11 @@ class TimeMachine extends PureComponent<Props> {
funcs, funcs,
script, script,
onAddNode, onAddNode,
onChangeArg,
onChangeScript, onChangeScript,
onSubmitScript, onSubmitScript,
onDeleteFuncNode, onDeleteFuncNode,
onGenerateScript,
} = this.props } = this.props
return ( return (
@ -42,7 +47,13 @@ class TimeMachine extends PureComponent<Props> {
/> />
<div className="func-nodes-container"> <div className="func-nodes-container">
{funcs.map(f => ( {funcs.map(f => (
<FuncNode key={f.id} func={f} onDelete={onDeleteFuncNode} /> <FuncNode
key={f.id}
func={f}
onChangeArg={onChangeArg}
onDelete={onDeleteFuncNode}
onGenerateScript={onGenerateScript}
/>
))} ))}
<FuncSelector funcs={this.funcNames} onAddNode={onAddNode} /> <FuncSelector funcs={this.funcNames} onAddNode={onAddNode} />
</div> </div>

View File

@ -0,0 +1,13 @@
export const INVALID = 'invalid'
export const NIL = 'nil'
export const STRING = 'string'
export const INT = 'int'
export const UINT = 'uint'
export const FLOAT = 'float'
export const BOOL = 'bool'
export const TIME = 'time'
export const DURATION = 'duration'
export const REGEXP = 'regexp'
export const ARRAY = 'array'
export const OBJECT = 'object'
export const FUNCTION = 'function'

View File

@ -7,8 +7,10 @@ import _ from 'lodash'
import TimeMachine, {Suggestion} from 'src/ifql/components/TimeMachine' import TimeMachine, {Suggestion} from 'src/ifql/components/TimeMachine'
import Walker from 'src/ifql/ast/walker' import Walker from 'src/ifql/ast/walker'
import {Func} from 'src/ifql/components/FuncArgs' import {Func} from 'src/ifql/components/FuncArgs'
import {InputArg} from 'src/ifql/components/FuncArgInput'
import {getSuggestions, getAST} from 'src/ifql/apis' import {getSuggestions, getAST} from 'src/ifql/apis'
import * as argTypes from 'src/ifql/constants/argumentTypes'
interface Links { interface Links {
self: string self: string
@ -70,9 +72,11 @@ export class IFQLPage extends PureComponent<Props, State> {
funcs={this.state.funcs} funcs={this.state.funcs}
suggestions={suggestions} suggestions={suggestions}
onAddNode={this.handleAddNode} onAddNode={this.handleAddNode}
onChangeArg={this.handleChangeArg}
onSubmitScript={this.getASTResponse} onSubmitScript={this.getASTResponse}
onChangeScript={this.handleChangeScript} onChangeScript={this.handleChangeScript}
onDeleteFuncNode={this.handleDeleteFuncNode} onDeleteFuncNode={this.handleDeleteFuncNode}
onGenerateScript={this.handleGenerateScript}
/> />
</div> </div>
</div> </div>
@ -80,6 +84,50 @@ export class IFQLPage extends PureComponent<Props, State> {
) )
} }
private handleGenerateScript = (): void => {
this.getASTResponse(this.funcsToScript)
}
private handleChangeArg = ({funcID, key, value}: InputArg): void => {
const funcs = this.state.funcs.map(f => {
if (f.id !== funcID) {
return f
}
const args = f.args.map(a => {
if (a.key === key) {
return {...a, value}
}
return a
})
return {...f, args}
})
this.setState({funcs})
}
private get funcsToScript(): string {
return this.state.funcs
.map(func => `${func.name}(${this.argsToScript(func.args)})`)
.join('\n\t|> ')
}
private argsToScript(args): string {
const withValues = args.filter(arg => arg.value)
return withValues
.map(({key, value, type}) => {
if (type === argTypes.STRING) {
return `${key}: "${value}"`
}
return `${key}: ${value}`
})
.join(', ')
}
private handleChangeScript = (script: string): void => { private handleChangeScript = (script: string): void => {
this.setState({script}) this.setState({script})
} }

View File

@ -0,0 +1,30 @@
import React from 'react'
import {shallow} from 'enzyme'
import FuncArg from 'src/ifql/components/FuncArg'
const setup = () => {
const props = {
funcID: '',
argKey: '',
value: '',
type: '',
onChangeArg: () => {},
onGenerateScript: () => {},
}
const wrapper = shallow(<FuncArg {...props} />)
return {
wrapper,
}
}
describe('IFQL.Components.FuncArg', () => {
describe('rendering', () => {
it('renders without errors', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
})

View File

@ -0,0 +1,78 @@
import React from 'react'
import {shallow} from 'enzyme'
import FuncArgInput from 'src/ifql/components/FuncArgInput'
const setup = (override?) => {
const props = {
funcID: '1',
argKey: 'db',
value: 'db1',
type: 'string',
onChangeArg: () => {},
onGenerateScript: () => {},
...override,
}
const wrapper = shallow(<FuncArgInput {...props} />)
return {
wrapper,
props,
}
}
describe('IFQL.Components.FuncArgInput', () => {
describe('rendering', () => {
it('renders without errors', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
describe('user interraction', () => {
describe('typing', () => {
describe('hitting enter', () => {
it('generates a new script when Enter is pressed', () => {
const onGenerateScript = jest.fn()
const preventDefault = jest.fn()
const {wrapper} = setup({onGenerateScript})
const input = wrapper.find('input')
input.simulate('keydown', {key: 'Enter', preventDefault})
expect(onGenerateScript).toHaveBeenCalledTimes(1)
expect(preventDefault).toHaveBeenCalledTimes(1)
})
it('it does not generate a new script when typing', () => {
const onGenerateScript = jest.fn()
const preventDefault = jest.fn()
const {wrapper} = setup({onGenerateScript})
const input = wrapper.find('input')
input.simulate('keydown', {key: 'a', preventDefault})
expect(onGenerateScript).not.toHaveBeenCalled()
expect(preventDefault).not.toHaveBeenCalled()
})
})
describe('changing the input value', () => {
it('calls onChangeArg', () => {
const onChangeArg = jest.fn()
const {wrapper, props} = setup({onChangeArg})
const input = wrapper.find('input')
const value = 'db2'
input.simulate('change', {target: {value}})
const {funcID, argKey} = props
expect(onChangeArg).toHaveBeenCalledWith({funcID, key: argKey, value})
})
})
})
})
})

View File

@ -11,6 +11,8 @@ const setup = () => {
onChangeScript: () => {}, onChangeScript: () => {},
onSubmitScript: () => {}, onSubmitScript: () => {},
onDeleteFuncNode: () => {}, onDeleteFuncNode: () => {},
onChangeArg: () => {},
onGenerateScript: () => {},
} }
const wrapper = shallow(<TimeMachine {...props} />) const wrapper = shallow(<TimeMachine {...props} />)