Replace Flux Builder with Data Explorer

Co-authored-by: Chris Henn <chris@chrishenn.net>
Co-authored-by: Iris Scholten <ischolten.is@gmail.com>
pull/10616/head
Christopher Henn 2018-10-10 10:14:54 -07:00 committed by Chris Henn
parent 8b17102a7d
commit bbd7153cca
83 changed files with 61 additions and 8020 deletions

View File

@ -0,0 +1,3 @@
.data-explorer {
padding: 32px;
}

View File

@ -1,19 +1,49 @@
import React, {PureComponent} from 'react'
// Libraries
import React, {PureComponent, ComponentClass} from 'react'
import {connect} from 'react-redux'
interface Props {}
// Components
import TimeMachine from 'src/shared/components/TimeMachine'
// Actions
import {setActiveTimeMachineID} from 'src/shared/actions/v2/timeMachines'
// Utils
import {DE_TIME_MACHINE_ID} from 'src/shared/constants/timeMachine'
interface StateProps {}
interface DispatchProps {
onSetActiveTimeMachineID: typeof setActiveTimeMachineID
}
interface PassedProps {}
interface State {}
class DataExplorer extends PureComponent<Props, State> {
constructor(props) {
super(props)
type Props = StateProps & DispatchProps & PassedProps
this.state = {}
class DataExplorer extends PureComponent<Props, State> {
public componentDidMount() {
const {onSetActiveTimeMachineID} = this.props
onSetActiveTimeMachineID(DE_TIME_MACHINE_ID)
}
public render() {
return null
return (
<div className="data-explorer">
<TimeMachine />
</div>
)
}
}
export default DataExplorer
const mdtp: DispatchProps = {
onSetActiveTimeMachineID: setActiveTimeMachineID,
}
export default connect(null, mdtp)(DataExplorer) as ComponentClass<
PassedProps,
State
>

View File

@ -1,21 +0,0 @@
export type Action = ActionUpdateScript
export enum ActionTypes {
UpdateScript = 'UPDATE_SCRIPT',
}
export interface ActionUpdateScript {
type: ActionTypes.UpdateScript
payload: {
script: string
}
}
export type UpdateScript = (script: string) => ActionUpdateScript
export const updateScript = (script: string): ActionUpdateScript => {
return {
type: ActionTypes.UpdateScript,
payload: {script},
}
}

View File

@ -1,142 +0,0 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import ExpressionNode from 'src/flux/components/ExpressionNode'
import VariableNode from 'src/flux/components/VariableNode'
import FuncSelector from 'src/flux/components/FuncSelector'
import BodyDelete from 'src/flux/components/BodyDelete'
import {funcNames} from 'src/flux/constants'
import {Body, Suggestion} from 'src/types/flux'
interface Props {
body: Body[]
suggestions: Suggestion[]
onAppendFrom: () => void
onAppendJoin: () => void
onDeleteBody: (bodyID: string) => void
}
class BodyBuilder extends PureComponent<Props> {
public render() {
const {body, onDeleteBody} = this.props
const bodybuilder = body.map((b, i) => {
if (b.declarations.length) {
return b.declarations.map(d => {
if (d.funcs) {
return (
<div className="declaration" key={i}>
<div className="func-node--wrapper">
<VariableNode name={d.name} assignedToQuery={true} />
<div className="func-node--menu">
<BodyDelete bodyID={b.id} onDeleteBody={onDeleteBody} />
</div>
</div>
<ExpressionNode
bodyID={b.id}
declarationID={d.id}
funcNames={this.funcNames}
funcs={d.funcs}
declarationsFromBody={this.declarationsFromBody}
isLastBody={this.isLastBody(i)}
onDeleteBody={onDeleteBody}
/>
</div>
)
}
return (
<div className="declaration" key={i}>
<div className="func-node--wrapper">
<VariableNode name={b.source} assignedToQuery={false} />
<div className="func-node--menu">
<BodyDelete
bodyID={b.id}
type="variable"
onDeleteBody={onDeleteBody}
/>
</div>
</div>
</div>
)
})
}
return (
<div className="declaration" key={i}>
<ExpressionNode
bodyID={b.id}
funcs={b.funcs}
funcNames={this.funcNames}
declarationsFromBody={this.declarationsFromBody}
isLastBody={this.isLastBody(i)}
onDeleteBody={onDeleteBody}
/>
</div>
)
})
return (
<FancyScrollbar className="body-builder--container" autoHide={true}>
<div className="body-builder">
{_.flatten(bodybuilder)}
<div className="declaration">
<FuncSelector
bodyID="fake-body-id"
declarationID="fake-declaration-id"
onAddNode={this.handleCreateNewBody}
funcs={this.newDeclarationFuncs}
connectorVisible={false}
/>
</div>
</div>
</FancyScrollbar>
)
}
private isLastBody(bodyIndex: number): boolean {
const {body} = this.props
return bodyIndex === body.length - 1
}
private get newDeclarationFuncs(): string[] {
const {body} = this.props
const declarationFunctions = [funcNames.FROM]
if (body.length > 1) {
declarationFunctions.push(funcNames.JOIN)
}
return declarationFunctions
}
private get declarationsFromBody(): string[] {
const {body} = this.props
const declarations = _.flatten(
body.map(b => {
if ('declarations' in b) {
const declarationsArray = b.declarations
return declarationsArray.map(da => da.name)
}
return []
})
)
return declarations
}
private handleCreateNewBody = name => {
if (name === funcNames.FROM) {
this.props.onAppendFrom()
}
if (name === funcNames.JOIN) {
this.props.onAppendJoin()
}
}
private get funcNames() {
return this.props.suggestions.map(f => f.name)
}
}
export default BodyBuilder

View File

@ -1,49 +0,0 @@
import React, {PureComponent} from 'react'
import ConfirmButton from 'src/shared/components/ConfirmButton'
type BodyType = 'variable' | 'query'
interface Props {
bodyID: string
type?: BodyType
onDeleteBody: (bodyID: string) => void
}
class BodyDelete extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
type: 'query',
}
public render() {
const {type} = this.props
if (type === 'variable') {
return (
<button
className="btn btn-sm btn-square btn-danger"
title="Delete Variable"
onClick={this.handleDelete}
>
<span className="icon remove" />
</button>
)
}
return (
<ConfirmButton
icon="trash"
type="btn-danger"
confirmText="Delete Query"
square={true}
confirmAction={this.handleDelete}
position="right"
/>
)
}
private handleDelete = (): void => {
this.props.onDeleteBody(this.props.bodyID)
}
}
export default BodyDelete

View File

@ -1,66 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import _ from 'lodash'
// Components
import DatabaseListItem from 'src/flux/components/DatabaseListItem'
// APIs
import {getBuckets} from 'src/shared/apis/v2/buckets'
// Types
import {Source} from 'src/types/v2'
import {NotificationAction} from 'src/types/notifications'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
source: Source
notify: NotificationAction
}
interface State {
databases: string[]
}
@ErrorHandling
class DatabaseList extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
databases: [],
}
}
public componentDidMount() {
this.getDatabases()
}
public async getDatabases() {
const {source} = this.props
try {
const buckets = await getBuckets(source.links.buckets)
const sorted = _.sortBy(buckets, b => b.name.toLocaleLowerCase())
const databases = sorted.map(db => db.name)
this.setState({databases})
} catch (err) {
console.error(err)
}
}
public render() {
const {databases} = this.state
const {source, notify} = this.props
return databases.map(db => {
return (
<DatabaseListItem key={db} db={db} source={source} notify={notify} />
)
})
}
}
export default DatabaseList

View File

@ -1,145 +0,0 @@
import React, {PureComponent, ChangeEvent, MouseEvent} from 'react'
import classnames from 'classnames'
import {CopyToClipboard} from 'react-copy-to-clipboard'
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values'
import TagList from 'src/flux/components/TagList'
import {
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {NotificationAction} from 'src/types'
import {Source} from 'src/types/v2'
interface Props {
db: string
source: Source
notify: NotificationAction
}
interface State {
isOpen: boolean
tags: string[]
searchTerm: string
}
class DatabaseListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
tags: [],
searchTerm: '',
}
}
public async componentDidMount() {
const {db, source} = this.props
try {
const response = await fetchTagKeys(source, db, [])
const tags = parseValuesColumn(response)
this.setState({tags})
} catch (error) {
console.error(error)
}
}
public render() {
const {db} = this.props
return (
<div className={this.className} onClick={this.handleClick}>
<div className="flux-schema--item">
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{db}
<span className="flux-schema--type">Bucket</span>
</div>
<CopyToClipboard text={db} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
{this.filterAndTagList}
</div>
)
}
private get tags(): string[] {
const {tags, searchTerm} = this.state
const term = searchTerm.toLocaleLowerCase()
return tags.filter(t => t.toLocaleLowerCase().includes(term))
}
private get className(): string {
return classnames('flux-schema-tree', {
expanded: this.state.isOpen,
})
}
private get filterAndTagList(): JSX.Element {
const {db, source, notify} = this.props
const {isOpen, searchTerm} = this.state
return (
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
<div className="flux-schema--filter">
<input
className="form-control input-xs"
placeholder={`Filter within ${db}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onClick={this.handleInputClick}
onChange={this.onSearch}
/>
</div>
<TagList
db={db}
source={source}
tags={this.tags}
filter={[]}
notify={notify}
/>
</div>
)
}
private handleClickCopy = e => {
e.stopPropagation()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(copyToClipboardSuccess(copiedText))
} else {
notify(copyToClipboardFailed(copiedText))
}
}
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
searchTerm: e.target.value,
})
}
private handleClick = () => {
this.setState({isOpen: !this.state.isOpen})
}
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
e.stopPropagation()
}
}
export default DatabaseListItem

View File

@ -1,183 +0,0 @@
import React, {PureComponent, Fragment} from 'react'
import {FluxContext} from 'src/flux/containers/FluxPage'
import FuncSelector from 'src/flux/components/FuncSelector'
import FuncNode from 'src/flux/components/FuncNode'
import YieldFuncNode from 'src/flux/components/YieldFuncNode'
import {getDeep} from 'src/utils/wrappers'
import {Func, Context} from 'src/types/flux'
interface Props {
funcNames: any[]
bodyID: string
funcs: Func[]
declarationID?: string
declarationsFromBody: string[]
isLastBody: boolean
onDeleteBody: (bodyID: string) => void
}
interface State {
nonYieldableIndexesToggled: {
[x: number]: boolean
}
}
// an Expression is a group of one or more functions
class ExpressionNode extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
nonYieldableIndexesToggled: {},
}
}
public render() {
const {
declarationID,
bodyID,
funcNames,
funcs,
declarationsFromBody,
onDeleteBody,
} = this.props
const {nonYieldableIndexesToggled} = this.state
return (
<FluxContext.Consumer>
{({
onDeleteFuncNode,
onAddNode,
onChangeArg,
onGenerateScript,
onToggleYield,
source,
data,
scriptUpToYield,
}: Context) => {
let isAfterRange = false
let isAfterFilter = false
return (
<>
{funcs.map((func, i) => {
if (func.name === 'yield') {
return null
}
if (func.name === 'range') {
isAfterRange = true
}
if (func.name === 'filter') {
isAfterFilter = true
}
const isYieldable = isAfterFilter && isAfterRange
const funcNode = (
<FuncNode
key={i}
index={i}
func={func}
funcs={funcs}
bodyID={bodyID}
source={source}
onChangeArg={onChangeArg}
onDelete={onDeleteFuncNode}
onToggleYield={onToggleYield}
isYieldable={isYieldable}
isYielding={this.isBeforeYielding(i)}
isYieldedInScript={this.isYieldNodeIndex(i + 1)}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody}
onToggleYieldWithLast={this.handleToggleYieldWithLast}
onDeleteBody={onDeleteBody}
/>
)
if (
nonYieldableIndexesToggled[i] ||
this.isYieldNodeIndex(i + 1)
) {
const script: string = scriptUpToYield(
bodyID,
declarationID,
i,
isYieldable
)
let yieldFunc = func
if (this.isYieldNodeIndex(i + 1)) {
yieldFunc = funcs[i + 1]
}
return (
<Fragment key={`${i}-notInScript`}>
{funcNode}
<YieldFuncNode
index={i}
func={yieldFunc}
data={data}
script={script}
bodyID={bodyID}
source={source}
declarationID={declarationID}
/>
</Fragment>
)
}
return funcNode
})}
<FuncSelector
bodyID={bodyID}
funcs={funcNames}
onAddNode={onAddNode}
declarationID={declarationID}
/>
</>
)
}}
</FluxContext.Consumer>
)
}
private isBeforeYielding(funcIndex: number): boolean {
const {nonYieldableIndexesToggled} = this.state
const beforeToggledLastYield = !!nonYieldableIndexesToggled[funcIndex]
if (beforeToggledLastYield) {
return true
}
return this.isYieldNodeIndex(funcIndex + 1)
}
private isYieldNodeIndex(funcIndex: number): boolean {
const {funcs} = this.props
const funcName = getDeep<string>(funcs, `${funcIndex}.name`, '')
return funcName === 'yield'
}
// if funcNode is not yieldable, add last before yield()
private handleToggleYieldWithLast = (funcNodeIndex: number): void => {
this.setState(({nonYieldableIndexesToggled}) => {
const isFuncYieldToggled = !!nonYieldableIndexesToggled[funcNodeIndex]
return {
nonYieldableIndexesToggled: {
...nonYieldableIndexesToggled,
[funcNodeIndex]: !isFuncYieldToggled,
},
}
})
}
}
export default ExpressionNode

View File

@ -1,129 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
// Components
import FilterTagList from 'src/flux/components/FilterTagList'
// APIs
import {getAST} from 'src/flux/apis'
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
// Utils
import parseValuesColumn from 'src/shared/parsing/flux/values'
import Walker from 'src/flux/ast/walker'
import {makeCancelable} from 'src/utils/promises'
// Types
import {Source} from 'src/types/v2'
import {Links, OnChangeArg, Func, FilterNode} from 'src/types/flux'
import {WrappedCancelablePromise} from 'src/types/promises'
interface Props {
links: Links
value: string
func: Func
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
db: string
source: Source
onGenerateScript: () => void
}
interface State {
tagKeys: string[]
nodes: FilterNode[]
ast: object
}
class FilterArgs extends PureComponent<Props, State> {
private fetchTagKeysResponse?: WrappedCancelablePromise<string>
constructor(props) {
super(props)
this.state = {
tagKeys: [],
nodes: [],
ast: {},
}
}
public async convertStringToNodes() {
const {links, value} = this.props
const ast = await getAST({url: links.ast, query: value})
const nodes = new Walker(ast).inOrderExpression
this.setState({nodes, ast})
}
public componentDidUpdate(prevProps) {
if (prevProps.value !== this.props.value) {
this.convertStringToNodes()
}
}
public async componentDidMount() {
try {
this.convertStringToNodes()
const response = await this.getTagKeys()
const tagKeys = parseValuesColumn(response)
this.setState({
tagKeys,
})
} catch (error) {
if (!error.isCanceled) {
console.error(error)
}
}
}
public componentWillUnmount() {
if (this.fetchTagKeysResponse) {
this.fetchTagKeysResponse.cancel()
}
}
public render() {
const {
db,
source,
onChangeArg,
func,
bodyID,
declarationID,
onGenerateScript,
} = this.props
const {nodes} = this.state
return (
<FilterTagList
db={db}
source={source}
tags={this.state.tagKeys}
filter={[]}
onChangeArg={onChangeArg}
func={func}
nodes={nodes}
bodyID={bodyID}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
)
}
private getTagKeys(): Promise<string> {
const {db, source} = this.props
this.fetchTagKeysResponse = makeCancelable(fetchTagKeys(source, db, []))
return this.fetchTagKeysResponse.promise
}
}
const mapStateToProps = ({links}) => {
return {links: links.query}
}
export default connect(mapStateToProps, null)(FilterArgs)

View File

@ -1,52 +0,0 @@
import React, {PureComponent} from 'react'
import {MemberExpressionNode} from 'src/types/flux'
type FilterNode = MemberExpressionNode
interface Props {
nodes: FilterNode[]
}
interface State {
tags: Tags
}
interface Tags {
[x: string]: string[]
}
export class FilterBuilder extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
tags: this.tags,
}
}
public render() {
return <div>Filter Builder</div>
}
private get tags(): Tags {
const {nodes} = this.props
return nodes.reduce((acc, node, i) => {
if (node.type === 'MemberExpression') {
const tagKey = node.property.name
const remainingNodes = nodes.slice(i + 1, nodes.length)
const tagValue = remainingNodes.find(n => {
return n.type !== 'Operator'
})
if (!(tagKey in acc)) {
return {...acc, [tagKey]: [tagValue.source]}
}
return {...acc, [tagKey]: [...acc[tagKey], tagValue.source]}
}
return acc
}, {})
}
}
export default FilterBuilder

View File

@ -1,48 +0,0 @@
import React, {PureComponent} from 'react'
import {FilterNode, MemberExpressionNode} from 'src/types/flux'
interface Props {
node: FilterNode
}
class FilterConditionNode extends PureComponent<Props> {
public render() {
const {node} = this.props
switch (node.type) {
case 'ObjectExpression': {
return <div className="flux-filter--key">{node.source}</div>
}
case 'MemberExpression': {
const memberNode = node as MemberExpressionNode
return (
<div className="flux-filter--key">{memberNode.property.name}</div>
)
}
case 'OpenParen': {
return <div className="flux-filter--paren-open" />
}
case 'CloseParen': {
return <div className="flux-filter--paren-close" />
}
case 'NumberLiteral':
case 'IntegerLiteral': {
return <div className="flux-filter--value number">{node.source}</div>
}
case 'BooleanLiteral': {
return <div className="flux-filter--value boolean">{node.source}</div>
}
case 'StringLiteral': {
return <div className="flux-filter--value string">{node.source}</div>
}
case 'Operator': {
return <div className="flux-filter--operator">{node.source}</div>
}
default: {
return <div />
}
}
}
}
export default FilterConditionNode

View File

@ -1,21 +0,0 @@
import React, {PureComponent} from 'react'
import {FilterNode} from 'src/types/flux'
import FilterConditionNode from 'src/flux/components/FilterConditionNode'
interface Props {
nodes: FilterNode[]
}
class FilterConditions extends PureComponent<Props> {
public render() {
return (
<>
{this.props.nodes.map((n, i) => (
<FilterConditionNode node={n} key={i} />
))}
</>
)
}
}
export default FilterConditions

View File

@ -1,58 +0,0 @@
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {getAST} from 'src/flux/apis'
import {Links, FilterNode} from 'src/types/flux'
import Walker from 'src/flux/ast/walker'
import FilterConditions from 'src/flux/components/FilterConditions'
interface Props {
filterString?: string
links: Links
}
interface State {
nodes: FilterNode[]
ast: object
}
export class FilterPreview extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
filterString: '',
}
constructor(props) {
super(props)
this.state = {
nodes: [],
ast: {},
}
}
public async componentDidMount() {
this.convertStringToNodes()
}
public async componentDidUpdate(prevProps, __) {
if (this.props.filterString !== prevProps.filterString) {
this.convertStringToNodes()
}
}
public async convertStringToNodes() {
const {links, filterString} = this.props
const ast = await getAST({url: links.ast, query: filterString})
const nodes = new Walker(ast).inOrderExpression
this.setState({nodes, ast})
}
public render() {
return <FilterConditions nodes={this.state.nodes} />
}
}
const mapStateToProps = ({links}) => {
return {links: links.query}
}
export default connect(mapStateToProps, null)(FilterPreview)

View File

@ -1,257 +0,0 @@
import React from 'react'
import {shallow} from 'enzyme'
import FilterTagList from 'src/flux/components/FilterTagList'
import FilterTagListItem from 'src/flux/components/FilterTagListItem'
const setup = (override?) => {
const props = {
db: 'telegraf',
tags: ['cpu', '_measurement'],
filter: [],
func: {
id: 'f1',
args: [{key: 'fn', value: '(r) => true'}],
},
nodes: [],
bodyID: 'b1',
declarationID: 'd1',
onChangeArg: () => {},
onGenerateScript: () => {},
...override,
}
const wrapper = shallow(<FilterTagList {...props} />)
return {
wrapper,
props,
}
}
describe('Flux.Components.FilterTagList', () => {
describe('rendering', () => {
it('renders without errors', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
it('renders a builder when the clause is parseable', () => {
const override = {
nodes: [{type: 'BooleanLiteral', source: 'true'}],
}
const {wrapper} = setup(override)
const builderContents = wrapper.find(FilterTagListItem)
expect(builderContents).not.toHaveLength(0)
})
it('renders a builder when the clause cannot be parsed', () => {
const override = {
nodes: [{type: 'Unparseable', source: 'baconcannon'}],
}
const {wrapper} = setup(override)
const builderContents = wrapper.find(FilterTagListItem)
expect(builderContents).toHaveLength(0)
})
describe('clause parseability', () => {
const parser = setup().wrapper.instance() as FilterTagList
it('recognizes a simple `true` body', () => {
const nodes = [{type: 'BooleanLiteral', source: 'true'}]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(true)
expect(clause).toEqual({})
})
it('allows for an empty node list', () => {
const nodes = []
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(true)
expect(clause).toEqual({})
})
it('extracts a tag condition equality', () => {
const nodes = [
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'Operator', source: '=='},
{type: 'StringLiteral', source: 'tagValue'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(true)
expect(clause).toEqual({
tagKey: [{key: 'tagKey', operator: '==', value: 'tagValue'}],
})
})
it('extracts a tag condition inequality', () => {
const nodes = [
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'Operator', source: '!='},
{type: 'StringLiteral', source: 'tagValue'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(true)
expect(clause).toEqual({
tagKey: [{key: 'tagKey', operator: '!=', value: 'tagValue'}],
})
})
it('groups like keys together', () => {
const nodes = [
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'Operator', source: '!='},
{type: 'StringLiteral', source: 'value1'},
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'Operator', source: '!='},
{type: 'StringLiteral', source: 'value2'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(true)
expect(clause).toEqual({
tagKey: [
{key: 'tagKey', operator: '!=', value: 'value1'},
{key: 'tagKey', operator: '!=', value: 'value2'},
],
})
})
it('separates conditions with different keys', () => {
const nodes = [
{type: 'MemberExpression', property: {name: 'key1'}},
{type: 'Operator', source: '!='},
{type: 'StringLiteral', source: 'value1'},
{type: 'MemberExpression', property: {name: 'key2'}},
{type: 'Operator', source: '!='},
{type: 'StringLiteral', source: 'value2'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(true)
expect(clause).toEqual({
key1: [{key: 'key1', operator: '!=', value: 'value1'}],
key2: [{key: 'key2', operator: '!=', value: 'value2'}],
})
})
it('cannot recognize other operators', () => {
const nodes = [
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'Operator', source: '=~'},
{type: 'StringLiteral', source: 'tagValue'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(false)
expect(clause).toEqual({})
})
it('requires that operators be consistent within a key group', () => {
const nodes = [
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'Operator', source: '=='},
{type: 'StringLiteral', source: 'tagValue'},
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'Operator', source: '!='},
{type: 'StringLiteral', source: 'tagValue'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(false)
expect(clause).toEqual({})
})
it('conditions must come in order to be recognizeable', () => {
const nodes = [
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'StringLiteral', source: 'tagValue'},
{type: 'Operator', source: '=~'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(false)
expect(clause).toEqual({})
})
it('does not recognize more esoteric types', () => {
const nodes = [
{type: 'ArrayExpression', property: {name: 'tagKey'}},
{type: 'MemberExpression', property: {name: 'tagKey'}},
{type: 'StringLiteral', source: 'tagValue'},
{type: 'Operator', source: '=~'},
]
const [clause, parseable] = parser.reduceNodesToClause(nodes, [])
expect(parseable).toBe(false)
expect(clause).toEqual({})
})
})
describe('building a filter string', () => {
const builder = setup().wrapper.instance() as FilterTagList
it('returns a simple filter with no conditions', () => {
const filterString = builder.buildFilterString({})
expect(filterString).toEqual('() => true')
})
it('renders a single condition', () => {
const clause = {
myKey: [{key: 'myKey', operator: '==', value: 'val1'}],
}
const filterString = builder.buildFilterString(clause)
expect(filterString).toEqual('(r) => (r.myKey == "val1")')
})
it('groups like keys together', () => {
const clause = {
myKey: [
{key: 'myKey', operator: '==', value: 'val1'},
{key: 'myKey', operator: '==', value: 'val2'},
],
}
const filterString = builder.buildFilterString(clause)
expect(filterString).toEqual(
'(r) => (r.myKey == "val1" OR r.myKey == "val2")'
)
})
it('joins conditions together with AND when operator is !=', () => {
const clause = {
myKey: [
{key: 'myKey', operator: '!=', value: 'val1'},
{key: 'myKey', operator: '!=', value: 'val2'},
],
}
const filterString = builder.buildFilterString(clause)
expect(filterString).toEqual(
'(r) => (r.myKey != "val1" AND r.myKey != "val2")'
)
})
it('always uses AND to join conditions across keys', () => {
const clause = {
key1: [
{key: 'key1', operator: '!=', value: 'val1'},
{key: 'key1', operator: '!=', value: 'val2'},
],
key2: [
{key: 'key2', operator: '==', value: 'val3'},
{key: 'key2', operator: '==', value: 'val4'},
],
}
const filterString = builder.buildFilterString(clause)
expect(filterString).toEqual(
'(r) => (r.key1 != "val1" AND r.key1 != "val2") AND (r.key2 == "val3" OR r.key2 == "val4")'
)
})
})
})

View File

@ -1,272 +0,0 @@
import React, {PureComponent, MouseEvent} from 'react'
import _ from 'lodash'
import {
OnChangeArg,
Func,
FilterClause,
FilterTagCondition,
FilterNode,
} from 'src/types/flux'
import {argTypes} from 'src/flux/constants'
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
import FilterTagListItem from 'src/flux/components/FilterTagListItem'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import {getDeep} from 'src/utils/wrappers'
import {SchemaFilter} from 'src/types'
import {Source} from 'src/types/v2'
interface Props {
db: string
source: Source
tags: string[]
filter: SchemaFilter[]
onChangeArg: OnChangeArg
func: Func
nodes: FilterNode[]
bodyID: string
declarationID: string
onGenerateScript: () => void
}
type ParsedClause = [FilterClause, boolean]
export default class FilterTagList extends PureComponent<Props> {
public get clauseIsParseable(): boolean {
const [, parseable] = this.reduceNodesToClause(this.props.nodes, [])
return parseable
}
public get clause(): FilterClause {
const [clause] = this.reduceNodesToClause(this.props.nodes, [])
return clause
}
public conditions(key: string, clause?): FilterTagCondition[] {
clause = clause || this.clause
return clause[key] || []
}
public operator(key: string, clause?): string {
const conditions = this.conditions(key, clause)
return getDeep<string>(conditions, '0.operator', '==')
}
public addCondition(condition: FilterTagCondition): FilterClause {
const conditions = this.conditions(condition.key)
return {
...this.clause,
[condition.key]: [...conditions, condition],
}
}
public removeCondition(condition: FilterTagCondition): FilterClause {
const conditions = this.conditions(condition.key)
const newConditions = _.reject(conditions, c => _.isEqual(c, condition))
return {
...this.clause,
[condition.key]: newConditions,
}
}
public buildFilterString(clause: FilterClause): string {
const funcBody = Object.entries(clause)
.filter(([__, conditions]) => conditions.length)
.map(([key, conditions]) => {
const joiner = this.operator(key, clause) === '==' ? ' OR ' : ' AND '
const subClause = conditions
.map(c => `r.${key} ${c.operator} "${c.value}"`)
.join(joiner)
return '(' + subClause + ')'
})
.join(' AND ')
return funcBody ? `(r) => ${funcBody}` : `() => true`
}
public handleChangeValue = (
key: string,
value: string,
selected: boolean
): void => {
const condition: FilterTagCondition = {
key,
operator: this.operator(key),
value,
}
const clause: FilterClause = selected
? this.addCondition(condition)
: this.removeCondition(condition)
const filterString: string = this.buildFilterString(clause)
this.updateFilterString(filterString)
}
public handleSetEquality = (key: string, equal: boolean): void => {
const operator = equal ? '==' : '!='
const clause: FilterClause = {
...this.clause,
[key]: this.conditions(key).map(c => ({...c, operator})),
}
const filterString: string = this.buildFilterString(clause)
this.updateFilterString(filterString)
}
public updateFilterString = (newFilterString: string): void => {
const {
func: {id},
bodyID,
declarationID,
} = this.props
this.props.onChangeArg({
funcID: id,
key: 'fn',
value: newFilterString,
declarationID,
bodyID,
generate: true,
})
}
public render() {
const {
db,
source,
tags,
filter,
bodyID,
declarationID,
onChangeArg,
onGenerateScript,
func: {id: funcID, args},
} = this.props
const {value, key: argKey} = args[0]
if (!this.clauseIsParseable) {
return (
<>
<p className="flux-filter--helper-text">
Unable to render expression as a Builder
</p>
<FuncArgTextArea
type={argTypes.STRING}
value={value}
argKey={argKey}
funcID={funcID}
bodyID={bodyID}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
</>
)
}
if (tags.length) {
return (
<FancyScrollbar className="flux-filter--fancyscroll" maxHeight={600}>
{tags.map(t => (
<FilterTagListItem
key={t}
db={db}
tagKey={t}
conditions={this.conditions(t)}
operator={this.operator(t)}
onChangeValue={this.handleChangeValue}
onSetEquality={this.handleSetEquality}
source={source}
filter={filter}
/>
))}
</FancyScrollbar>
)
}
return (
<div className="flux-schema-tree">
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
<div className="no-results">No tag keys found.</div>
</div>
</div>
)
}
public reduceNodesToClause(
nodes,
conditions: FilterTagCondition[]
): ParsedClause {
if (!nodes.length) {
return this.constructClause(conditions)
} else if (this.noConditions(nodes, conditions)) {
return [{}, true]
} else if (
['OpenParen', 'CloseParen', 'Operator'].includes(nodes[0].type)
) {
return this.skipNode(nodes, conditions)
} else if (this.conditionExtractable(nodes)) {
return this.extractCondition(nodes, conditions)
} else {
// Unparseable
return [{}, false]
}
}
private constructClause(conditions: FilterTagCondition[]): ParsedClause {
const clause = _.groupBy(conditions, condition => condition.key)
if (this.validateClause(clause)) {
return [clause, true]
} else {
return [{}, false]
}
}
private validateClause(clause) {
return Object.values(clause).every((conditions: FilterTagCondition[]) =>
conditions.every(c => conditions[0].operator === c.operator)
)
}
private noConditions(nodes, conditions) {
return (
!conditions.length &&
nodes.length === 1 &&
nodes[0].type === 'BooleanLiteral' &&
nodes[0].source === 'true'
)
}
private skipNode([, ...nodes], conditions) {
return this.reduceNodesToClause(nodes, conditions)
}
private conditionExtractable(nodes): boolean {
return (
nodes.length >= 3 &&
nodes[0].type === 'MemberExpression' &&
nodes[1].type === 'Operator' &&
this.supportedOperator(nodes[1].source) &&
nodes[2].type === 'StringLiteral'
)
}
private supportedOperator(operator): boolean {
return operator === '==' || operator === '!='
}
private extractCondition(
[keyNode, operatorNode, valueNode, ...nodes],
conditions
) {
const condition: FilterTagCondition = {
key: keyNode.property.name,
operator: operatorNode.source,
value: valueNode.source.replace(/"/g, ''),
}
return this.reduceNodesToClause(nodes, [...conditions, condition])
}
private handleClick(e: MouseEvent<HTMLDivElement>) {
e.stopPropagation()
}
}

View File

@ -1,329 +0,0 @@
import React, {
PureComponent,
CSSProperties,
ChangeEvent,
MouseEvent,
} from 'react'
import _ from 'lodash'
import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries'
import {explorer} from 'src/flux/constants'
import {
SetFilterTagValue,
SetEquality,
FilterTagCondition,
} from 'src/types/flux'
import parseValuesColumn from 'src/shared/parsing/flux/values'
import FilterTagValueList from 'src/flux/components/FilterTagValueList'
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import {SchemaFilter, RemoteDataState} from 'src/types'
import {Source} from 'src/types/v2'
interface Props {
tagKey: string
onSetEquality: SetEquality
onChangeValue: SetFilterTagValue
conditions: FilterTagCondition[]
operator: string
db: string
source: Source
filter: SchemaFilter[]
}
interface State {
isOpen: boolean
loadingAll: RemoteDataState
loadingSearch: RemoteDataState
loadingMore: RemoteDataState
tagValues: string[]
searchTerm: string
limit: number
count: number | null
}
export default class FilterTagListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
loadingAll: RemoteDataState.NotStarted,
loadingSearch: RemoteDataState.NotStarted,
loadingMore: RemoteDataState.NotStarted,
tagValues: [],
count: null,
searchTerm: '',
limit: explorer.TAG_VALUES_LIMIT,
}
this.debouncedOnSearch = _.debounce(() => {
this.searchTagValues()
this.getCount()
}, 250)
}
public renderEqualitySwitcher() {
const {operator} = this.props
if (!this.state.isOpen) {
return null
}
return (
<ul className="nav nav-tablist nav-tablist-xs">
<li
className={operator === '==' ? 'active' : ''}
onClick={this.setEquality(true)}
>
=
</li>
<li
className={operator === '!=' ? 'active' : ''}
onClick={this.setEquality(false)}
>
!=
</li>
</ul>
)
}
public render() {
const {tagKey, db, filter} = this.props
const {tagValues, searchTerm, loadingMore, count, limit} = this.state
const selectedValues = this.props.conditions.map(c => c.value)
return (
<div className={this.className}>
<div className="flux-schema--item" onClick={this.handleClick}>
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{tagKey}
<span className="flux-schema--type">Tag Key</span>
</div>
{this.renderEqualitySwitcher()}
</div>
{this.state.isOpen && (
<div className="flux-schema--children">
<div
className="flux-schema--header"
onClick={this.handleInputClick}
>
<div className="flux-schema--filter">
<input
className="form-control input-sm"
placeholder={`Filter within ${tagKey}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onChange={this.onSearch}
/>
{this.isSearching && (
<LoadingSpinner style={this.spinnerStyle} />
)}
</div>
{!!count && (
<div className="flux-schema--count">{`${count} Tag Values`}</div>
)}
</div>
{this.isLoading && <LoaderSkeleton />}
{!this.isLoading && (
<FilterTagValueList
db={db}
tagKey={tagKey}
filter={filter}
values={tagValues}
selectedValues={selectedValues}
loadMoreCount={this.loadMoreCount}
shouldShowMoreValues={limit < count}
onChangeValue={this.props.onChangeValue}
onLoadMoreValues={this.handleLoadMoreValues}
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
/>
)}
</div>
)}
</div>
)
}
private setEquality(equal: boolean) {
return (e): void => {
e.stopPropagation()
const {tagKey} = this.props
this.props.onSetEquality(tagKey, equal)
}
}
private get spinnerStyle(): CSSProperties {
return {
position: 'absolute',
right: '15px',
top: '6px',
}
}
private get isSearching(): boolean {
return this.state.loadingSearch === RemoteDataState.Loading
}
private get isLoading(): boolean {
return this.state.loadingAll === RemoteDataState.Loading
}
private onSearch = (e: ChangeEvent<HTMLInputElement>): void => {
const searchTerm = e.target.value
this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () =>
this.debouncedOnSearch()
)
}
private debouncedOnSearch() {} // See constructor
private handleInputClick = (e: MouseEvent<HTMLDivElement>): void => {
e.stopPropagation()
}
private searchTagValues = async () => {
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingSearch: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingSearch: RemoteDataState.Error})
}
}
private getAllTagValues = async () => {
this.setState({loadingAll: RemoteDataState.Loading})
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingAll: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingAll: RemoteDataState.Error})
}
}
private getMoreTagValues = async () => {
this.setState({loadingMore: RemoteDataState.Loading})
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingMore: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingMore: RemoteDataState.Error})
}
}
private getTagValues = async () => {
const {db, source, tagKey, filter} = this.props
const {searchTerm, limit} = this.state
const response = await fetchTagValues({
source,
db,
filter,
tagKey,
limit,
searchTerm,
})
return parseValuesColumn(response)
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (this.isFetchable) {
this.getCount()
this.getAllTagValues()
}
this.setState({isOpen: !this.state.isOpen})
}
private handleLoadMoreValues = (): void => {
const {limit} = this.state
this.setState(
{limit: limit + explorer.TAG_VALUES_LIMIT},
this.getMoreTagValues
)
}
private async getCount() {
const {source, db, filter, tagKey} = this.props
const {limit, searchTerm} = this.state
try {
const response = await fetchTagValues({
source,
db,
filter,
tagKey,
limit,
searchTerm,
count: true,
})
const parsed = parseValuesColumn(response)
if (parsed.length !== 1) {
// We expect to never reach this state; instead, the Flux server should
// return a non-200 status code is handled earlier (after fetching).
// This return guards against some unexpected behavior---the Flux server
// returning a 200 status code but ALSO having an error in the CSV
// response body
return
}
const count = Number(parsed[0])
this.setState({count})
} catch (error) {
console.error(error)
}
}
private get loadMoreCount(): number {
const {count, limit} = this.state
return Math.min(Math.abs(count - limit), explorer.TAG_VALUES_LIMIT)
}
private get isFetchable(): boolean {
const {isOpen, loadingAll} = this.state
return (
!isOpen &&
(loadingAll === RemoteDataState.NotStarted ||
loadingAll !== RemoteDataState.Error)
)
}
private get className(): string {
const {isOpen} = this.state
const openClass = isOpen ? 'expanded' : ''
return `flux-schema-tree ${openClass}`
}
}

View File

@ -1,67 +0,0 @@
import React, {PureComponent, MouseEvent} from 'react'
import _ from 'lodash'
import FilterTagValueListItem from 'src/flux/components/FilterTagValueListItem'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
import {SchemaFilter} from 'src/types'
import {SetFilterTagValue} from 'src/types/flux'
interface Props {
db: string
tagKey: string
values: string[]
selectedValues: string[]
onChangeValue: SetFilterTagValue
filter: SchemaFilter[]
isLoadingMoreValues: boolean
onLoadMoreValues: () => void
shouldShowMoreValues: boolean
loadMoreCount: number
}
export default class FilterTagValueList extends PureComponent<Props> {
public render() {
const {values, tagKey, shouldShowMoreValues} = this.props
return (
<>
{values.map((v, i) => (
<FilterTagValueListItem
key={i}
value={v}
selected={_.includes(this.props.selectedValues, v)}
tagKey={tagKey}
onChangeValue={this.props.onChangeValue}
/>
))}
{shouldShowMoreValues && (
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<button
className="btn btn-xs btn-default increase-values-limit"
onClick={this.handleClick}
>
{this.buttonValue}
</button>
</div>
</div>
)}
</>
)
}
private handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
this.props.onLoadMoreValues()
}
private get buttonValue(): string | JSX.Element {
const {isLoadingMoreValues, loadMoreCount, tagKey} = this.props
if (isLoadingMoreValues) {
return <LoadingSpinner />
}
return `Load next ${loadMoreCount} values for ${tagKey}`
}
}

View File

@ -1,49 +0,0 @@
import React, {PureComponent, MouseEvent} from 'react'
import {SetFilterTagValue} from 'src/types/flux'
interface Props {
tagKey: string
value: string
onChangeValue: SetFilterTagValue
selected: boolean
}
class FilterTagValueListItem extends PureComponent<Props> {
constructor(props) {
super(props)
}
public render() {
const {value} = this.props
return (
<div
className="flux-schema-tree flux-schema--child"
onClick={this.handleClick}
>
<div className={this.listItemClasses}>
<div className="flex-schema-item-group">
<div className="query-builder--checkbox" />
{value}
<span className="flux-schema--type">Tag Value</span>
</div>
</div>
</div>
)
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
const {tagKey, value, selected} = this.props
e.stopPropagation()
this.props.onChangeValue(tagKey, value, !selected)
}
private get listItemClasses() {
const baseClasses = 'flux-schema--item query-builder--list-item'
return this.props.selected ? baseClasses + ' active' : baseClasses
}
}
export default FilterTagValueListItem

View File

@ -1,93 +0,0 @@
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import _ from 'lodash'
import FluxForm from 'src/flux/components/FluxForm'
import {Service, Notification} from 'src/types'
import {
fluxUpdated,
fluxNotUpdated,
fluxNameAlreadyTaken,
} from 'src/shared/copy/notifications'
import {UpdateServiceAsync} from 'src/shared/actions/services'
import {FluxFormMode} from 'src/flux/constants/connection'
interface Props {
service: Service
services: Service[]
onDismiss?: () => void
updateService: UpdateServiceAsync
notify: (message: Notification) => void
}
interface State {
service: Service
}
class FluxEdit extends PureComponent<Props, State> {
public static getDerivedStateFromProps(nextProps: Props, prevState: State) {
if (_.isEmpty(prevState.service) && !_.isEmpty(nextProps.service)) {
return {service: nextProps.service}
}
return null
}
constructor(props: Props) {
super(props)
this.state = {
service: this.props.service,
}
}
public render() {
return (
<FluxForm
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
mode={FluxFormMode.EDIT}
/>
)
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
const {value, name} = e.target
const update = {[name]: value}
this.setState({service: {...this.state.service, ...update}})
}
private handleSubmit = async (
e: FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault()
const {notify, onDismiss, updateService, services} = this.props
const {service} = this.state
service.name = service.name.trim()
let isNameTaken = false
services.forEach(s => {
if (s.name === service.name && s.id !== service.id) {
isNameTaken = true
}
})
if (isNameTaken) {
notify(fluxNameAlreadyTaken(service.name))
return
}
try {
await updateService(service)
} catch (error) {
notify(fluxNotUpdated(error.message))
return
}
notify(fluxUpdated)
if (onDismiss) {
onDismiss()
}
}
}
export default FluxEdit

View File

@ -1,74 +0,0 @@
import React, {ChangeEvent, PureComponent} from 'react'
import _ from 'lodash'
import Input from 'src/shared/components/KapacitorFormInput'
import {NewService} from 'src/types'
import {FluxFormMode} from 'src/flux/constants/connection'
interface Props {
service: NewService
mode: FluxFormMode
onSubmit: (e: ChangeEvent<HTMLFormElement>) => void
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
}
class FluxForm extends PureComponent<Props> {
public render() {
const {service, onSubmit, onInputChange} = this.props
const name = _.get(service, 'name', '')
return (
<form onSubmit={onSubmit}>
<Input
name="url"
label="Flux URL"
value={this.url}
placeholder={this.url}
onChange={onInputChange}
customClass="col-sm-6"
/>
<Input
name="name"
label="Name"
value={name}
placeholder={name}
onChange={onInputChange}
maxLength={33}
customClass="col-sm-6"
/>
<div className="form-group form-group-submit col-xs-12 text-center">
{this.saveButton}
</div>
</form>
)
}
private get saveButton(): JSX.Element {
const {mode} = this.props
let text = 'Connect'
if (mode === FluxFormMode.EDIT) {
text = 'Save Changes'
}
return (
<button
className="btn btn-success"
type="submit"
data-test="submit-button"
>
<span className="icon checkmark" />
{text}
</button>
)
}
private get url(): string {
const {service} = this.props
return _.get(service, 'url', '')
}
}
export default FluxForm

View File

@ -1,58 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
// Components
import Dygraph from 'src/shared/components/dygraph/Dygraph'
// Utils
import {fluxTablesToDygraph} from 'src/shared/parsing/flux/dygraph'
// Actions
import {setHoverTime as setHoverTimeAction} from 'src/dashboards/actions/v2/hoverTime'
// Constants
import {DEFAULT_LINE_COLORS} from 'src/shared/constants/graphColorPalettes'
// Types
import {FluxTable} from 'src/types'
import {ViewType} from 'src/types/v2/dashboards'
interface Props {
data: FluxTable[]
setHoverTime: (time: string) => void
}
class FluxGraph extends PureComponent<Props> {
public render() {
const {dygraphsData, labels} = fluxTablesToDygraph(this.props.data)
return (
<div className="yield-node--graph">
<Dygraph
labels={labels}
type={ViewType.Line}
staticLegend={false}
dygraphSeries={{}}
options={this.options}
timeSeries={dygraphsData}
colors={DEFAULT_LINE_COLORS}
handleSetHoverTime={this.props.setHoverTime}
/>
</div>
)
}
private get options() {
return {
axisLineColor: '#383846',
gridLineColor: '#383846',
}
}
}
const mdtp = {
setHoverTime: setHoverTimeAction,
}
export default connect(null, mdtp)(FluxGraph)

View File

@ -1,131 +0,0 @@
import React, {PureComponent, ChangeEvent, FormEvent} from 'react'
import FluxForm from 'src/flux/components/FluxForm'
import {NewService, Source, Service, Notification} from 'src/types'
import {
fluxCreated,
fluxNotCreated,
fluxNameAlreadyTaken,
} from 'src/shared/copy/notifications'
import {
CreateServiceAsync,
SetActiveServiceAsync,
} from 'src/shared/actions/services'
import {FluxFormMode} from 'src/flux/constants/connection'
import {getDeep} from 'src/utils/wrappers'
interface Props {
source: Source
services: Service[]
setActiveFlux?: SetActiveServiceAsync
onDismiss?: () => void
createService: CreateServiceAsync
router?: {push: (url: string) => void}
notify: (message: Notification) => void
}
interface State {
service: NewService
}
const port = 8093
class FluxNew extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
service: this.defaultService,
}
}
public render() {
return (
<FluxForm
service={this.state.service}
onSubmit={this.handleSubmit}
onInputChange={this.handleInputChange}
mode={FluxFormMode.NEW}
/>
)
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>): void => {
const {value, name} = e.target
const update = {[name]: value}
this.setState({service: {...this.state.service, ...update}})
}
private handleSubmit = async (
e: FormEvent<HTMLFormElement>
): Promise<void> => {
e.preventDefault()
const {
notify,
router,
source,
services,
onDismiss,
setActiveFlux,
createService,
} = this.props
const {service} = this.state
service.name = service.name.trim()
const isNameTaken = services.some(s => s.name === service.name)
if (isNameTaken) {
notify(fluxNameAlreadyTaken(service.name))
return
}
try {
const active = this.activeService
const s = await createService(source, service)
if (setActiveFlux) {
await setActiveFlux(source, s, active)
}
if (router) {
router.push(`/sources/${source.id}/flux/${s.id}/edit`)
}
} catch (error) {
notify(fluxNotCreated(error.message))
return
}
notify(fluxCreated)
if (onDismiss) {
onDismiss()
}
}
private get defaultService(): NewService {
return {
name: 'Flux',
url: this.url,
username: '',
insecureSkipVerify: false,
type: 'flux',
metadata: {
active: true,
},
}
}
private get activeService(): Service {
const {services} = this.props
const activeService = services.find(s => {
return getDeep<boolean>(s, 'metadata.active', false)
})
return activeService || services[0]
}
private get url(): string {
const parser = document.createElement('a')
parser.href = this.props.source.url
return `${parser.protocol}//${parser.hostname}:${port}`
}
}
export default FluxNew

View File

@ -1,33 +0,0 @@
import React from 'react'
import {shallow} from 'enzyme'
import FromDatabaseDropdown from 'src/flux/components/FromDatabaseDropdown'
import {source} from 'src/sources/resources'
jest.mock('src/shared/apis/metaQuery', () => require('mocks/flux/apis'))
const setup = () => {
const props = {
funcID: '1',
argKey: 'db',
value: 'db1',
bodyID: '2',
declarationID: '1',
source,
onChangeArg: () => {},
}
const wrapper = shallow(<FromDatabaseDropdown {...props} />)
return {
wrapper,
}
}
describe('Flux.Components.FromDatabaseDropdown', () => {
describe('rendering', () => {
it('renders without errors', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
})

View File

@ -1,87 +0,0 @@
import React, {PureComponent} from 'react'
import {showDatabases} from 'src/shared/apis/metaQuery'
import showDatabasesParser from 'src/shared/parsing/showDatabases'
import Dropdown from 'src/shared/components/Dropdown'
import {OnChangeArg} from 'src/types/flux'
import {Source} from 'src/types/v2'
interface Props {
funcID: string
argKey: string
value: string
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
source: Source
}
interface State {
dbs: string[]
}
interface DropdownItem {
text: string
}
class FromDatabaseDropdown extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
dbs: [],
}
}
public async componentDidMount() {
const {source} = this.props
try {
// (watts): TODO: hit actual buckets API
const {data} = await showDatabases(source.links.buckets)
const {databases} = showDatabasesParser(data)
const sorted = databases.sort()
this.setState({dbs: sorted})
} catch (err) {
console.error(err)
}
}
public render() {
const {value, argKey} = this.props
return (
<div className="func-arg">
<label className="func-arg--label">{argKey}</label>
<Dropdown
selected={value}
className="from--dropdown dropdown-160 func-arg--value"
menuClass="dropdown-astronaut"
buttonColor="btn-default"
items={this.items}
onChoose={this.handleChooseDatabase}
/>
</div>
)
}
private handleChooseDatabase = (item: DropdownItem): void => {
const {argKey, funcID, onChangeArg, bodyID, declarationID} = this.props
onChangeArg({
funcID,
key: argKey,
value: item.text,
bodyID,
declarationID,
generate: true,
})
}
private get items(): DropdownItem[] {
return this.state.dbs.map(text => ({text}))
}
}
export default FromDatabaseDropdown

View File

@ -1,36 +0,0 @@
import React from 'react'
import {shallow} from 'enzyme'
import FuncArg from 'src/flux/components/FuncArg'
import {source} from 'src/sources/resources'
const setup = () => {
const props = {
funcID: '',
bodyID: '',
funcName: '',
declarationID: '',
argKey: '',
args: [],
value: '',
type: '',
source,
onChangeArg: () => {},
onGenerateScript: () => {},
}
const wrapper = shallow(<FuncArg {...props} />)
return {
wrapper,
}
}
describe('Flux.Components.FuncArg', () => {
describe('rendering', () => {
it('renders without errors', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
})

View File

@ -1,148 +0,0 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import FuncArgInput from 'src/flux/components/FuncArgInput'
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
import FuncArgBool from 'src/flux/components/FuncArgBool'
import {ErrorHandling} from 'src/shared/decorators/errors'
import FromDatabaseDropdown from 'src/flux/components/FromDatabaseDropdown'
import {funcNames, argTypes} from 'src/flux/constants'
import {OnChangeArg, Arg, OnGenerateScript} from 'src/types/flux'
import {Source} from 'src/types/v2'
interface Props {
source: Source
funcName: string
funcID: string
argKey: string
args: Arg[]
value: string | boolean | {[x: string]: string}
type: string
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
onGenerateScript: OnGenerateScript
}
@ErrorHandling
class FuncArg extends PureComponent<Props> {
public render() {
const {
argKey,
value,
type,
bodyID,
funcID,
source,
funcName,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
if (funcName === funcNames.FROM) {
return (
<FromDatabaseDropdown
source={source}
argKey={argKey}
funcID={funcID}
value={this.value}
bodyID={bodyID}
declarationID={declarationID}
onChangeArg={onChangeArg}
/>
)
}
switch (type) {
case argTypes.STRING:
case argTypes.DURATION:
case argTypes.TIME:
case argTypes.REGEXP:
case argTypes.FLOAT:
case argTypes.INT:
case argTypes.UINT:
case argTypes.INVALID:
case argTypes.ARRAY: {
return (
<FuncArgInput
type={type}
value={this.value}
argKey={argKey}
funcID={funcID}
bodyID={bodyID}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
autoFocus={this.isFirstArg}
/>
)
}
case argTypes.BOOL: {
return (
<FuncArgBool
value={this.boolValue}
argKey={argKey}
bodyID={bodyID}
funcID={funcID}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
)
}
case argTypes.FUNCTION: {
return (
<FuncArgTextArea
type={type}
value={this.value}
argKey={argKey}
funcID={funcID}
bodyID={bodyID}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
)
}
case argTypes.NIL: {
// TODO: handle nil type
return (
<div className="func-arg">
<label className="func-arg--label">{argKey}</label>
<div className="func-arg--value">{value}</div>
</div>
)
}
default: {
return (
<div className="func-arg">
<label className="func-arg--label">{argKey}</label>
<div className="func-arg--value">{value}</div>
</div>
)
}
}
}
private get value(): string {
return this.props.value.toString()
}
private get boolValue(): boolean {
return this.props.value === true
}
private get isFirstArg(): boolean {
const {args, argKey} = this.props
const firstArg = _.first(args)
return firstArg.key === argKey
}
}
export default FuncArg

View File

@ -1,53 +0,0 @@
import React, {PureComponent} from 'react'
import SlideToggle from 'src/clockface/components/slide_toggle/SlideToggle'
import {ComponentColor} from 'src/clockface/types'
import {OnChangeArg} from 'src/types/flux'
interface Props {
argKey: string
value: boolean
funcID: string
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
onGenerateScript: () => void
}
class FuncArgBool extends PureComponent<Props> {
public render() {
return (
<div className="func-arg">
<label className="func-arg--label">{this.props.argKey}</label>
<div className="func-arg--value">
<SlideToggle
active={this.props.value}
onChange={this.handleToggleClick}
color={ComponentColor.Success}
/>
</div>
</div>
)
}
private handleToggleClick = (): void => {
const {
argKey,
funcID,
bodyID,
onChangeArg,
declarationID,
value,
} = this.props
onChangeArg({
key: argKey,
value: !value,
funcID,
bodyID,
declarationID,
generate: true,
})
}
}
export default FuncArgBool

View File

@ -1,78 +0,0 @@
import React from 'react'
import {shallow} from 'enzyme'
import FuncArgInput from 'src/flux/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('Flux.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

@ -1,67 +0,0 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeArg, OnGenerateScript} from 'src/types/flux'
interface Props {
funcID: string
argKey: string
value: string
type: string
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
onGenerateScript: OnGenerateScript
autoFocus?: boolean
}
@ErrorHandling
class FuncArgInput extends PureComponent<Props> {
public render() {
const {argKey, value, type, autoFocus} = this.props
return (
<div className="func-arg">
<label className="func-arg--label" htmlFor={argKey}>
{argKey}
</label>
<div className="func-arg--value">
<input
name={argKey}
value={value}
placeholder={type}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
type="text"
className="form-control input-sm"
spellCheck={false}
autoComplete="off"
autoFocus={autoFocus}
/>
</div>
</div>
)
}
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.key !== 'Enter') {
return
}
e.preventDefault()
this.props.onGenerateScript()
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const {funcID, argKey, bodyID, declarationID} = this.props
this.props.onChangeArg({
funcID,
key: argKey,
value: e.target.value,
declarationID,
bodyID,
})
}
}
export default FuncArgInput

View File

@ -1,102 +0,0 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeArg} from 'src/types/flux'
interface Props {
funcID: string
argKey: string
value: string
type: string
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
onGenerateScript: () => void
inputType?: string
}
interface State {
height: string
}
@ErrorHandling
class FuncArgTextArea extends PureComponent<Props, State> {
private ref: React.RefObject<HTMLTextAreaElement>
constructor(props: Props) {
super(props)
this.ref = React.createRef()
this.state = {
height: '100px',
}
}
public componentDidMount() {
this.setState({height: this.height})
}
public render() {
const {argKey, value, type} = this.props
return (
<div className="func-arg">
<label className="func-arg--label" htmlFor={argKey}>
{argKey}
</label>
<div className="func-arg--value">
<textarea
className="func-arg--textarea form-control input-sm"
name={argKey}
value={value}
spellCheck={false}
autoFocus={true}
autoComplete="off"
placeholder={type}
ref={this.ref}
onChange={this.handleChange}
onKeyUp={this.handleKeyUp}
style={this.textAreaStyle}
/>
</div>
</div>
)
}
private get textAreaStyle() {
const style = {
height: this.state.height,
}
return style
}
private get height(): string {
const ref = this.ref.current
if (!ref) {
return '200px'
}
const {scrollHeight} = ref
return `${scrollHeight}px`
}
private handleKeyUp = (e: KeyboardEvent<HTMLTextAreaElement>) => {
const target = e.target as HTMLTextAreaElement
const height = `${target.scrollHeight}px`
this.setState({height})
}
private handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const {funcID, argKey, bodyID, declarationID} = this.props
this.props.onChangeArg({
funcID,
key: argKey,
value: e.target.value,
declarationID,
bodyID,
})
}
}
export default FuncArgTextArea

View File

@ -1,144 +0,0 @@
import React, {PureComponent, MouseEvent} from 'react'
import FuncArg from 'src/flux/components/FuncArg'
import {OnChangeArg} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Func, OnGenerateScript} from 'src/types/flux'
import {funcNames} from 'src/flux/constants'
import JoinArgs from 'src/flux/components/JoinArgs'
import FilterArgs from 'src/flux/components/FilterArgs'
import {Source} from 'src/types/v2'
import {getDeep} from 'src/utils/wrappers'
interface Props {
func: Func
source: Source
bodyID: string
onChangeArg: OnChangeArg
declarationID: string
onGenerateScript: OnGenerateScript
declarationsFromBody: string[]
onStopPropagation: (e: MouseEvent<HTMLElement>) => void
}
@ErrorHandling
export default class FuncArgs extends PureComponent<Props> {
public render() {
const {onStopPropagation} = this.props
return (
<div className="func-node--editor" onClick={onStopPropagation}>
<div className="func-node--connector" />
<div className="func-args">{this.renderArguments}</div>
<div className="func-arg--buttons">{this.build}</div>
</div>
)
}
get renderArguments(): JSX.Element | JSX.Element[] {
const {func} = this.props
const {name: funcName} = func
if (funcName === funcNames.JOIN) {
return this.renderJoin
}
if (funcName === funcNames.FILTER) {
return this.renderFilter
}
return this.renderGeneralArguments
}
get renderGeneralArguments(): JSX.Element | JSX.Element[] {
const {
func,
bodyID,
source,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
const {name: funcName, id: funcID, args} = func
return args.map(({key, value, type}) => (
<FuncArg
key={key}
args={args}
type={type}
argKey={key}
value={value}
bodyID={bodyID}
funcID={funcID}
funcName={funcName}
source={source}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
))
}
get renderFilter(): JSX.Element {
const {
func,
bodyID,
source,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
const value = getDeep<string>(func.args, '0.value', '')
return (
<FilterArgs
value={value}
func={func}
bodyID={bodyID}
declarationID={declarationID}
onChangeArg={onChangeArg}
onGenerateScript={onGenerateScript}
source={source}
db={'telegraf'}
/>
)
}
get renderJoin(): JSX.Element {
const {
func,
bodyID,
onChangeArg,
declarationID,
onGenerateScript,
declarationsFromBody,
} = this.props
return (
<JoinArgs
func={func}
bodyID={bodyID}
declarationID={declarationID}
onChangeArg={onChangeArg}
declarationsFromBody={declarationsFromBody}
onGenerateScript={onGenerateScript}
/>
)
}
get build(): JSX.Element {
const {func, onGenerateScript} = this.props
if (func.name === funcNames.FILTER) {
return (
<div
className="btn btn-sm btn-primary func-node--build"
onClick={onGenerateScript}
>
Build
</div>
)
}
return null
}
}

View File

@ -1,93 +0,0 @@
import React, {PureComponent} from 'react'
import uuid from 'uuid'
import _ from 'lodash'
import {Func} from 'src/types/flux'
import {funcNames} from 'src/flux/constants'
import FilterPreview from 'src/flux/components/FilterPreview'
import {getDeep} from 'src/utils/wrappers'
interface Props {
func: Func
}
export default class FuncArgsPreview extends PureComponent<Props> {
public render() {
return <div className="func-node--preview">{this.summarizeArguments}</div>
}
private get summarizeArguments(): JSX.Element | JSX.Element[] {
const {func} = this.props
const {args} = func
if (!args) {
return
}
if (func.name === funcNames.FILTER) {
const value = getDeep<string>(args, '0.value', '')
if (!value) {
return this.colorizedArguments
}
return <FilterPreview filterString={value} />
}
return this.colorizedArguments
}
private get colorizedArguments(): JSX.Element | JSX.Element[] {
const {func} = this.props
const {args} = func
return args.map((arg, i): JSX.Element => {
if (!arg.value) {
return
}
const separator = i === 0 ? null : ', '
let argValue
if (arg.type === 'object') {
const valueMap = _.map(arg.value, (value, key) => `${key}:${value}`)
argValue = `{${valueMap.join(', ')}}`
} else {
argValue = `${arg.value}`
}
return (
<React.Fragment key={uuid.v4()}>
{separator}
{arg.key}: {this.colorArgType(argValue, arg.type)}
</React.Fragment>
)
})
}
private colorArgType = (argument: string, type: string): JSX.Element => {
switch (type) {
case 'time':
case 'number':
case 'period':
case 'duration':
case 'array': {
return <span className="func-arg--number">{argument}</span>
}
case 'bool': {
return <span className="func-arg--boolean">{argument}</span>
}
case 'string': {
return <span className="func-arg--string">"{argument}"</span>
}
case 'object': {
return <span className="func-arg--object">{argument}</span>
}
case 'invalid': {
return <span className="func-arg--invalid">{argument}</span>
}
default: {
return <span>{argument}</span>
}
}
}
}

View File

@ -1,59 +0,0 @@
import React, {SFC, ChangeEvent, KeyboardEvent} from 'react'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import FuncSelectorInput from 'src/shared/components/FuncSelectorInput'
import FuncListItem from 'src/flux/components/FuncListItem'
interface Props {
inputText: string
onInputChange: (e: ChangeEvent<HTMLInputElement>) => void
onKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void
onAddNode: (name: string) => void
funcs: string[]
selectedFunc: string
onSetSelectedFunc: (name: string) => void
}
const FuncList: SFC<Props> = ({
inputText,
onAddNode,
onKeyDown,
onInputChange,
funcs,
selectedFunc,
onSetSelectedFunc,
}) => {
return (
<div className="flux-func--autocomplete">
<FuncSelectorInput
onFilterChange={onInputChange}
onFilterKeyPress={onKeyDown}
searchTerm={inputText}
/>
<ul className="flux-func--list">
<FancyScrollbar
autoHide={false}
autoHeight={true}
maxHeight={240}
className="fancy-scroll--func-selector"
>
{!!funcs.length ? (
funcs.map((func, i) => (
<FuncListItem
key={i}
name={func}
onAddNode={onAddNode}
selectedFunc={selectedFunc}
onSetSelectedFunc={onSetSelectedFunc}
/>
))
) : (
<div className="flux-func--item empty">No matches</div>
)}
</FancyScrollbar>
</ul>
</div>
)
}
export default FuncList

View File

@ -1,37 +0,0 @@
import React, {PureComponent} from 'react'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
name: string
onAddNode: (name: string) => void
selectedFunc: string
onSetSelectedFunc: (name: string) => void
}
@ErrorHandling
export default class FuncListItem extends PureComponent<Props> {
public render() {
return (
<li
onClick={this.handleClick}
onMouseEnter={this.handleMouseEnter}
className={`flux-func--item ${this.activeClass}`}
>
{this.props.name}
</li>
)
}
private get activeClass(): string {
const {name, selectedFunc} = this.props
return name === selectedFunc ? 'active' : ''
}
private handleMouseEnter = () => {
this.props.onSetSelectedFunc(this.props.name)
}
private handleClick = () => {
this.props.onAddNode(this.props.name)
}
}

View File

@ -1,234 +0,0 @@
import React, {PureComponent, MouseEvent} from 'react'
import classnames from 'classnames'
import _ from 'lodash'
import {getDeep} from 'src/utils/wrappers'
import BodyDelete from 'src/flux/components/BodyDelete'
import FuncArgs from 'src/flux/components/FuncArgs'
import FuncArgsPreview from 'src/flux/components/FuncArgsPreview'
import {
OnGenerateScript,
OnDeleteFuncNode,
OnChangeArg,
OnToggleYield,
Func,
} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {Source} from 'src/types/v2'
interface Props {
func: Func
funcs: Func[]
source: Source
bodyID: string
index: number
declarationID?: string
onDelete: OnDeleteFuncNode
onToggleYield: OnToggleYield
onChangeArg: OnChangeArg
onGenerateScript: OnGenerateScript
onToggleYieldWithLast: (funcNodeIndex: number) => void
declarationsFromBody: string[]
isYielding: boolean
isYieldable: boolean
onDeleteBody: (bodyID: string) => void
isYieldedInScript: boolean
}
interface State {
editing: boolean
}
@ErrorHandling
export default class FuncNode extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
declarationID: '',
}
constructor(props) {
super(props)
this.state = {
editing: this.isLast,
}
}
public render() {
const {func} = this.props
return (
<>
<div className="func-node--wrapper">
<div
className={this.nodeClassName}
onClick={this.handleToggleEdit}
title="Edit function arguments"
>
<div className="func-node--connector" />
<div className="func-node--name">{func.name}</div>
<FuncArgsPreview func={func} />
</div>
{this.funcMenu}
</div>
{this.funcArgs}
</>
)
}
private get funcArgs(): JSX.Element {
const {
func,
bodyID,
source,
isYielding,
onChangeArg,
declarationID,
onGenerateScript,
declarationsFromBody,
} = this.props
const {editing} = this.state
if (!editing || isYielding) {
return
}
return (
<FuncArgs
func={func}
bodyID={bodyID}
source={source}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody}
onStopPropagation={this.handleClickArgs}
/>
)
}
private get funcMenu(): JSX.Element {
return (
<div className="func-node--menu">
{this.yieldToggleButton}
{this.deleteButton}
</div>
)
}
private get yieldToggleButton(): JSX.Element {
const {isYielding} = this.props
if (isYielding) {
return (
<button
className="btn btn-sm btn-square btn-warning"
onClick={this.handleToggleYield}
title="Hide Data Table"
>
<span className="icon eye-closed" />
</button>
)
}
return (
<button
className="btn btn-sm btn-square btn-warning"
onClick={this.handleToggleYield}
title="See Data Table returned by this Function"
>
<span className="icon eye-open" />
</button>
)
}
private get deleteButton(): JSX.Element {
const {func, bodyID, onDeleteBody} = this.props
if (func.name === 'from') {
return <BodyDelete onDeleteBody={onDeleteBody} bodyID={bodyID} />
}
return (
<button
className="btn btn-sm btn-square btn-danger"
onClick={this.handleDelete}
title="Delete this Function"
>
<span className="icon remove" />
</button>
)
}
private get nodeClassName(): string {
const {isYielding} = this.props
const {editing} = this.state
return classnames('func-node', {active: isYielding || editing})
}
private handleDelete = (): void => {
const {
func,
funcs,
index,
bodyID,
declarationID,
isYielding,
isYieldedInScript,
onToggleYieldWithLast,
} = this.props
let yieldFuncNodeID: string
if (isYieldedInScript) {
yieldFuncNodeID = getDeep<string>(funcs, `${index + 1}.id`, '')
} else if (isYielding) {
onToggleYieldWithLast(index)
}
this.props.onDelete({
funcID: func.id,
yieldNodeID: yieldFuncNodeID,
bodyID,
declarationID,
})
}
private handleToggleEdit = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation()
this.setState({editing: !this.state.editing})
}
private handleToggleYield = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation()
const {
onToggleYield,
index,
bodyID,
declarationID,
onToggleYieldWithLast,
isYieldable,
isYieldedInScript,
} = this.props
if (isYieldedInScript || isYieldable) {
onToggleYield(bodyID, declarationID, index)
} else {
onToggleYieldWithLast(index)
}
}
private handleClickArgs = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation()
}
private get isLast(): boolean {
const {funcs, func} = this.props
const lastFunc = _.last(funcs)
return lastFunc.id === func.id
}
}

View File

@ -1,157 +0,0 @@
import React from 'react'
import {shallow} from 'enzyme'
import {FuncSelector} from 'src/flux/components/FuncSelector'
import FuncSelectorInput from 'src/shared/components/FuncSelectorInput'
import FuncListItem from 'src/flux/components/FuncListItem'
import FuncList from 'src/flux/components/FuncList'
const setup = (override = {}) => {
const props = {
funcs: ['count', 'range'],
bodyID: '1',
declarationID: '2',
onAddNode: () => {},
...override,
}
const wrapper = shallow(<FuncSelector {...props} />)
return {
props,
wrapper,
}
}
describe('Flux.Components.FuncsButton', () => {
describe('rendering', () => {
it('renders', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
describe('the function list', () => {
it('does not render the list of funcs by default', () => {
const {wrapper} = setup()
const list = wrapper.find({'data-test': 'func-li'})
expect(list.exists()).toBe(false)
})
})
})
describe('user interraction', () => {
describe('clicking the add function button', () => {
it('displays the list of functions', () => {
const {wrapper, props} = setup()
const [func1, func2] = props.funcs
const dropdownButton = wrapper.find('button')
dropdownButton.simulate('click')
const list = wrapper
.find(FuncList)
.dive()
.find(FuncListItem)
const first = list.first().dive()
const last = list.last().dive()
expect(list.length).toBe(2)
expect(first.text()).toBe(func1)
expect(last.text()).toBe(func2)
})
})
describe('filtering the list', () => {
it('displays the filtered funcs', () => {
const {wrapper, props} = setup()
const [func1, func2] = props.funcs
const dropdownButton = wrapper.find('button')
dropdownButton.simulate('click')
let list = wrapper
.find(FuncList)
.dive()
.find(FuncListItem)
const first = list.first().dive()
const last = list.last().dive()
expect(list.length).toBe(2)
expect(first.text()).toBe(func1)
expect(last.text()).toBe(func2)
const input = wrapper
.find(FuncList)
.dive()
.find(FuncSelectorInput)
.dive()
.find('input')
input.simulate('change', {target: {value: 'ra'}})
wrapper.update()
list = wrapper
.find(FuncList)
.dive()
.find(FuncListItem)
const func = list.first()
expect(list.length).toBe(1)
expect(func.dive().text()).toBe(func2)
})
})
describe('exiting the list', () => {
it('closes when ESC is pressed', () => {
const {wrapper} = setup()
const dropdownButton = wrapper.find('button')
dropdownButton.simulate('click')
let list = wrapper.find(FuncList).dive()
const input = list
.find(FuncSelectorInput)
.dive()
.find('input')
input.simulate('keyDown', {key: 'Escape'})
wrapper.update()
list = wrapper.find(FuncList)
expect(list.exists()).toBe(false)
})
})
describe('selecting a function with the keyboard', () => {
describe('ArrowDown', () => {
it('adds a function to the page', () => {
const onAddNode = jest.fn()
const {wrapper, props} = setup({onAddNode})
const [, func2] = props.funcs
const {bodyID, declarationID} = props
const dropdownButton = wrapper.find('button')
dropdownButton.simulate('click')
const list = wrapper.find(FuncList).dive()
const input = list
.find(FuncSelectorInput)
.dive()
.find('input')
input.simulate('keyDown', {key: 'ArrowDown'})
input.simulate('keyDown', {key: 'Enter'})
expect(onAddNode).toHaveBeenCalledWith(func2, bodyID, declarationID)
})
})
})
})
})

View File

@ -1,176 +0,0 @@
import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react'
import _ from 'lodash'
import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside'
import FuncList from 'src/flux/components/FuncList'
import {OnAddNode} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface State {
isOpen: boolean
inputText: string
selectedFunc: string
}
interface Props {
funcs: string[]
bodyID: string
declarationID: string
onAddNode: OnAddNode
connectorVisible?: boolean
}
@ErrorHandling
export class FuncSelector extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
connectorVisible: true,
}
constructor(props) {
super(props)
this.state = {
isOpen: false,
inputText: '',
selectedFunc: '',
}
}
public render() {
const {isOpen, inputText, selectedFunc} = this.state
const {connectorVisible} = this.props
return (
<ClickOutside onClickOutside={this.handleClickOutside}>
<div className={this.className}>
{connectorVisible && <div className="func-selector--connector" />}
{isOpen ? (
<FuncList
inputText={inputText}
onAddNode={this.handleAddNode}
funcs={this.availableFuncs}
onInputChange={this.handleInputChange}
onKeyDown={this.handleKeyDown}
selectedFunc={selectedFunc}
onSetSelectedFunc={this.handleSetSelectedFunc}
/>
) : (
<button
className="btn btn-square btn-primary btn-sm flux-func--button"
onClick={this.handleOpenList}
tabIndex={0}
>
<span className="icon plus" />
</button>
)}
</div>
</ClickOutside>
)
}
private get className(): string {
const {isOpen} = this.state
return classnames('flux-func--selector', {open: isOpen})
}
private handleCloseList = () => {
this.setState({isOpen: false, selectedFunc: ''})
}
private handleAddNode = (name: string) => {
const {bodyID, declarationID} = this.props
this.handleCloseList()
this.props.onAddNode(name, bodyID, declarationID)
}
private get availableFuncs() {
return this.props.funcs.filter(f =>
f.toLowerCase().includes(this.state.inputText)
)
}
private setSelectedFunc = () => {
const {selectedFunc} = this.state
const isSelectedVisible = !!this.availableFuncs.find(
a => a === selectedFunc
)
const newSelectedFunc =
this.availableFuncs.length > 0 ? this.availableFuncs[0] : ''
this.setState({
selectedFunc: isSelectedVisible ? selectedFunc : newSelectedFunc,
})
}
private handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState(
{
inputText: e.target.value,
},
this.setSelectedFunc
)
}
private handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
const {selectedFunc} = this.state
const selectedFuncExists = selectedFunc !== ''
if (e.key === 'Enter' && selectedFuncExists) {
return this.handleAddNode(selectedFunc)
}
if (e.key === 'Escape' || e.key === 'Tab') {
return this.handleCloseList()
}
if (e.key === 'ArrowUp' && selectedFuncExists) {
// get index of selectedFunc in availableFuncs
const selectedIndex = _.findIndex(
this.availableFuncs,
func => func === selectedFunc
)
const previousIndex = selectedIndex - 1
// if there is selectedIndex - 1 in availableFuncs make that the new SelectedFunc
if (previousIndex >= 0) {
return this.setState({selectedFunc: this.availableFuncs[previousIndex]})
}
// if not then keep selectedFunc as is
}
if (e.key === 'ArrowDown' && selectedFuncExists) {
// get index of selectedFunc in availableFuncs
const selectedIndex = _.findIndex(
this.availableFuncs,
func => func === selectedFunc
)
const nextIndex = selectedIndex + 1
// if there is selectedIndex + 1 in availableFuncs make that the new SelectedFunc
if (nextIndex < this.availableFuncs.length) {
return this.setState({selectedFunc: this.availableFuncs[nextIndex]})
}
// if not then keep selectedFunc as is
}
}
private handleSetSelectedFunc = funcName => {
this.setState({selectedFunc: funcName})
}
private handleOpenList = () => {
const {funcs} = this.props
this.setState({
isOpen: true,
inputText: '',
selectedFunc: funcs[0],
})
}
private handleClickOutside = () => {
this.handleCloseList()
}
}
export default FuncSelector

View File

@ -1,154 +0,0 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import Dropdown from 'src/shared/components/Dropdown'
import FuncArgInput from 'src/flux/components/FuncArgInput'
import FuncArgTextArea from 'src/flux/components/FuncArgTextArea'
import {getDeep} from 'src/utils/wrappers'
import {OnChangeArg, Func, Arg} from 'src/types/flux'
import {argTypes} from 'src/flux/constants'
interface Props {
func: Func
bodyID: string
declarationID: string
onChangeArg: OnChangeArg
declarationsFromBody: string[]
onGenerateScript: () => void
}
interface DropdownItem {
text: string
}
class JoinArgs extends PureComponent<Props> {
constructor(props: Props) {
super(props)
}
public render() {
const {
func,
bodyID,
onChangeArg,
declarationID,
onGenerateScript,
} = this.props
return (
<>
<div className="func-arg">
<label className="func-arg--label">tables</label>
<Dropdown
selected={this.table1Value}
className="from--dropdown dropdown-100 func-arg--value"
menuClass="dropdown-astronaut"
buttonColor="btn-default"
items={this.items}
onChoose={this.handleChooseTable1}
/>
<Dropdown
selected={this.table2Value}
className="from--dropdown dropdown-100 func-arg--value"
menuClass="dropdown-astronaut"
buttonColor="btn-default"
items={this.items}
onChoose={this.handleChooseTable2}
/>
</div>
<div className="func-arg">
<FuncArgInput
value={this.onValue}
argKey={'on'}
bodyID={bodyID}
funcID={func.id}
type={argTypes.STRING}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
</div>
<div className="func-arg">
<FuncArgTextArea
type={argTypes.FUNCTION}
value={this.fnValue}
argKey={'fn'}
funcID={func.id}
bodyID={bodyID}
onChangeArg={onChangeArg}
declarationID={declarationID}
onGenerateScript={onGenerateScript}
/>
</div>
</>
)
}
private handleChooseTable1 = (item: DropdownItem): void => {
this.handleChooseTables(item.text, this.table2Value)
}
private handleChooseTable2 = (item: DropdownItem): void => {
this.handleChooseTables(this.table1Value, item.text)
}
private handleChooseTables = (table1: string, table2: string): void => {
const {
onChangeArg,
bodyID,
declarationID,
func,
onGenerateScript,
} = this.props
onChangeArg({
funcID: func.id,
bodyID,
declarationID,
key: 'tables',
value: {[table1]: table1, [table2]: table2},
generate: true,
})
onGenerateScript()
}
private get items(): DropdownItem[] {
return this.props.declarationsFromBody.map(d => ({text: d}))
}
private get argsArray(): Arg[] {
const {func} = this.props
return getDeep<Arg[]>(func, 'args', [])
}
private get onValue(): string {
const onObject = this.argsArray.find(a => a.key === 'on')
return onObject.value.toString()
}
private get fnValue(): string {
const fnObject = this.argsArray.find(a => a.key === 'fn')
return fnObject.value.toString()
}
private get table1Value(): string {
const tables = this.argsArray.find(a => a.key === 'tables')
if (tables) {
const keys = _.keys(tables.value)
return getDeep<string>(keys, '0', '')
}
return ''
}
private get table2Value(): string {
const tables = this.argsArray.find(a => a.key === 'tables')
if (tables) {
const keys = _.keys(tables.value)
return getDeep<string>(keys, '1', getDeep<string>(keys, '0', ''))
}
return ''
}
}
export default JoinArgs

View File

@ -1,42 +0,0 @@
import React, {SFC, MouseEvent, CSSProperties} from 'react'
import _ from 'lodash'
const handleClick = (e: MouseEvent<HTMLDivElement>): void => {
e.stopPropagation()
}
const randomSize = (): CSSProperties => {
const width = _.random(60, 200)
return {width: `${width}px`}
}
const LoaderSkeleton: SFC = () => {
return (
<>
<div
className="flux-schema-tree flux-schema--child"
onClick={handleClick}
>
<div className="flux-schema--item no-hover">
<div className="flux-schema--expander" />
<div className="flux-schema--item-skeleton" style={randomSize()} />
</div>
</div>
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<div className="flux-schema--expander" />
<div className="flux-schema--item-skeleton" style={randomSize()} />
</div>
</div>
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<div className="flux-schema--expander" />
<div className="flux-schema--item-skeleton" style={randomSize()} />
</div>
</div>
</>
)
}
export default LoaderSkeleton

View File

@ -1,19 +0,0 @@
import React, {SFC, CSSProperties} from 'react'
interface Props {
style?: CSSProperties
}
const LoadingSpinner: SFC<Props> = ({style}) => {
return (
<div className="loading-spinner" style={style}>
<div className="spinner">
<div className="bounce1" />
<div className="bounce2" />
<div className="bounce3" />
</div>
</div>
)
}
export default LoadingSpinner

View File

@ -1,39 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
// Components
import DatabaseList from 'src/flux/components/DatabaseList'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
// Actions
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Types
import {Source} from 'src/types/v2'
import {NotificationAction} from 'src/types/notifications'
interface Props {
source: Source
notify: NotificationAction
}
class SchemaExplorer extends PureComponent<Props> {
public render() {
const {source, notify} = this.props
return (
<div className="flux-schema-explorer">
<FancyScrollbar>
<DatabaseList source={source} notify={notify} />
</FancyScrollbar>
</div>
)
}
}
const mdtp = {
notify: notifyAction,
}
export default connect(null, mdtp)(SchemaExplorer)

View File

@ -1,37 +0,0 @@
import React, {PureComponent, MouseEvent} from 'react'
interface Props {
schemaType: string
name: string
}
interface State {
isOpen: boolean
}
export default class SchemaItem extends PureComponent<Props, State> {
public render() {
const {schemaType} = this.props
return (
<div className={this.className}>
<div className="flux-schema--item" onClick={this.handleClick}>
<div className="flux-schema--expander" />
{name}
<span className="flux-schema--type">{schemaType}</span>
</div>
</div>
)
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
this.setState({isOpen: !this.state.isOpen})
}
private get className(): string {
const {isOpen} = this.state
const openClass = isOpen ? 'expanded' : ''
return `flux-schema-tree flux-schema--child ${openClass}`
}
}

View File

@ -1,50 +0,0 @@
import React, {PureComponent, MouseEvent} from 'react'
import TagListItem from 'src/flux/components/TagListItem'
import {SchemaFilter} from 'src/types'
import {NotificationAction} from 'src/types/notifications'
import {Source} from 'src/types/v2'
interface Props {
db: string
source: Source
tags: string[]
filter: SchemaFilter[]
notify: NotificationAction
}
export default class TagList extends PureComponent<Props> {
public render() {
const {db, source, tags, filter, notify} = this.props
if (tags.length) {
return (
<>
{tags.map(t => (
<TagListItem
key={t}
db={db}
tagKey={t}
source={source}
filter={filter}
notify={notify}
/>
))}
</>
)
}
return (
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover" onClick={this.handleClick}>
<div className="no-results">No more tag keys.</div>
</div>
</div>
)
}
private handleClick(e: MouseEvent<HTMLDivElement>) {
e.stopPropagation()
}
}

View File

@ -1,334 +0,0 @@
// Libraries
import React, {
PureComponent,
CSSProperties,
ChangeEvent,
MouseEvent,
} from 'react'
import _ from 'lodash'
import {CopyToClipboard} from 'react-copy-to-clipboard'
// Components
import TagValueList from 'src/flux/components/TagValueList'
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
// APIs
import {tagValues as fetchTagValues} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values'
// Constants
import {
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
import {explorer} from 'src/flux/constants'
// Types
import {NotificationAction} from 'src/types'
import {SchemaFilter, RemoteDataState} from 'src/types'
import {Source} from 'src/types/v2'
interface Props {
tagKey: string
db: string
source: Source
filter: SchemaFilter[]
notify: NotificationAction
}
interface State {
isOpen: boolean
loadingAll: RemoteDataState
loadingSearch: RemoteDataState
loadingMore: RemoteDataState
tagValues: string[]
searchTerm: string
limit: number
count: number | null
}
export default class TagListItem extends PureComponent<Props, State> {
private debouncedOnSearch: () => void
constructor(props) {
super(props)
this.state = {
isOpen: false,
loadingAll: RemoteDataState.NotStarted,
loadingSearch: RemoteDataState.NotStarted,
loadingMore: RemoteDataState.NotStarted,
tagValues: [],
count: null,
searchTerm: '',
limit: explorer.TAG_VALUES_LIMIT,
}
this.debouncedOnSearch = _.debounce(() => {
this.searchTagValues()
this.getCount()
}, 250)
}
public render() {
const {tagKey, db, source, filter, notify} = this.props
const {
tagValues,
searchTerm,
loadingMore,
count,
limit,
isOpen,
} = this.state
return (
<div className={this.className}>
<div className="flux-schema--item" onClick={this.handleClick}>
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{tagKey}
<span className="flux-schema--type">Tag Key</span>
</div>
<CopyToClipboard text={tagKey} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
<div className="flux-schema--header" onClick={this.handleInputClick}>
<div className="flux-schema--filter">
<input
className="form-control input-xs"
placeholder={`Filter within ${tagKey}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onChange={this.onSearch}
/>
{this.isSearching && <LoadingSpinner style={this.spinnerStyle} />}
</div>
{this.count}
</div>
{this.isLoading && <LoaderSkeleton />}
{!this.isLoading && (
<TagValueList
db={db}
notify={notify}
source={source}
values={tagValues}
tagKey={tagKey}
filter={filter}
onLoadMoreValues={this.handleLoadMoreValues}
isLoadingMoreValues={loadingMore === RemoteDataState.Loading}
shouldShowMoreValues={limit < count}
loadMoreCount={this.loadMoreCount}
/>
)}
</div>
</div>
)
}
private get count(): JSX.Element {
const {count} = this.state
if (!count) {
return
}
let pluralizer = 's'
if (count === 1) {
pluralizer = ''
}
return (
<div className="flux-schema--count">{`${count} Tag Value${pluralizer}`}</div>
)
}
private get spinnerStyle(): CSSProperties {
return {
position: 'absolute',
right: '18px',
top: '11px',
}
}
private get isSearching(): boolean {
return this.state.loadingSearch === RemoteDataState.Loading
}
private get isLoading(): boolean {
return this.state.loadingAll === RemoteDataState.Loading
}
private onSearch = (e: ChangeEvent<HTMLInputElement>): void => {
const searchTerm = e.target.value
this.setState({searchTerm, loadingSearch: RemoteDataState.Loading}, () =>
this.debouncedOnSearch()
)
}
private handleInputClick = (e: MouseEvent<HTMLDivElement>): void => {
e.stopPropagation()
}
private searchTagValues = async () => {
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingSearch: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingSearch: RemoteDataState.Error})
}
}
private getAllTagValues = async () => {
this.setState({loadingAll: RemoteDataState.Loading})
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingAll: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingAll: RemoteDataState.Error})
}
}
private getMoreTagValues = async () => {
this.setState({loadingMore: RemoteDataState.Loading})
try {
const tagValues = await this.getTagValues()
this.setState({
tagValues,
loadingMore: RemoteDataState.Done,
})
} catch (error) {
console.error(error)
this.setState({loadingMore: RemoteDataState.Error})
}
}
private getTagValues = async () => {
const {db, source, tagKey, filter} = this.props
const {searchTerm, limit} = this.state
const response = await fetchTagValues({
source,
db,
filter,
tagKey,
limit,
searchTerm,
})
return parseValuesColumn(response)
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (this.isFetchable) {
this.getCount()
this.getAllTagValues()
}
this.setState({isOpen: !this.state.isOpen})
}
private handleLoadMoreValues = (): void => {
const {limit} = this.state
this.setState(
{limit: limit + explorer.TAG_VALUES_LIMIT},
this.getMoreTagValues
)
}
private handleClickCopy = e => {
e.stopPropagation()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(copyToClipboardSuccess(copiedText))
} else {
notify(copyToClipboardFailed(copiedText))
}
}
private async getCount() {
const {source, db, filter, tagKey} = this.props
const {limit, searchTerm} = this.state
try {
const response = await fetchTagValues({
source,
db,
filter,
tagKey,
limit,
searchTerm,
count: true,
})
const parsed = parseValuesColumn(response)
if (parsed.length !== 1) {
// We expect to never reach this state; instead, the Flux server should
// return a non-200 status code is handled earlier (after fetching).
// This return guards against some unexpected behavior---the Flux server
// returning a 200 status code but ALSO having an error in the CSV
// response body
return
}
const count = Number(parsed[0])
this.setState({count})
} catch (error) {
console.error(error)
}
}
private get loadMoreCount(): number {
const {count, limit} = this.state
return Math.min(Math.abs(count - limit), explorer.TAG_VALUES_LIMIT)
}
private get isFetchable(): boolean {
const {isOpen, loadingAll} = this.state
return (
!isOpen &&
(loadingAll === RemoteDataState.NotStarted ||
loadingAll === RemoteDataState.Error)
)
}
private get className(): string {
const {isOpen} = this.state
const openClass = isOpen ? 'expanded' : ''
return `flux-schema-tree flux-schema--child ${openClass}`
}
}

View File

@ -1,81 +0,0 @@
// Libraries
import React, {PureComponent, MouseEvent} from 'react'
// Components
import TagValueListItem from 'src/flux/components/TagValueListItem'
import LoadingSpinner from 'src/flux/components/LoadingSpinner'
// Types
import {SchemaFilter} from 'src/types'
import {NotificationAction} from 'src/types/notifications'
import {Source} from 'src/types/v2'
interface Props {
source: Source
db: string
tagKey: string
values: string[]
filter: SchemaFilter[]
isLoadingMoreValues: boolean
onLoadMoreValues: () => void
shouldShowMoreValues: boolean
loadMoreCount: number
notify: NotificationAction
}
export default class TagValueList extends PureComponent<Props> {
public render() {
const {
db,
notify,
source,
values,
tagKey,
filter,
shouldShowMoreValues,
} = this.props
return (
<>
{values.map((v, i) => (
<TagValueListItem
key={i}
db={db}
value={v}
tagKey={tagKey}
source={source}
filter={filter}
notify={notify}
/>
))}
{shouldShowMoreValues && (
<div className="flux-schema-tree flux-schema--child">
<div className="flux-schema--item no-hover">
<button
className="btn btn-xs btn-default increase-values-limit"
onClick={this.handleClick}
>
{this.buttonValue}
</button>
</div>
</div>
)}
</>
)
}
private handleClick = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation()
this.props.onLoadMoreValues()
}
private get buttonValue(): string | JSX.Element {
const {isLoadingMoreValues, loadMoreCount, tagKey} = this.props
if (isLoadingMoreValues) {
return <LoadingSpinner />
}
return `Load next ${loadMoreCount} values for ${tagKey}`
}
}

View File

@ -1,187 +0,0 @@
// Libraries
import React, {PureComponent, MouseEvent, ChangeEvent} from 'react'
import {CopyToClipboard} from 'react-copy-to-clipboard'
// Components
import TagList from 'src/flux/components/TagList'
import LoaderSkeleton from 'src/flux/components/LoaderSkeleton'
// APIs
import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
// Utils
import parseValuesColumn from 'src/shared/parsing/flux/values'
// Constants
import {
copyToClipboardSuccess,
copyToClipboardFailed,
} from 'src/shared/copy/notifications'
// Types
import {Source} from 'src/types/v2'
import {SchemaFilter, RemoteDataState, NotificationAction} from 'src/types'
interface Props {
db: string
source: Source
tagKey: string
value: string
filter: SchemaFilter[]
notify: NotificationAction
}
interface State {
isOpen: boolean
tags: string[]
loading: RemoteDataState
searchTerm: string
}
class TagValueListItem extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
isOpen: false,
tags: [],
loading: RemoteDataState.NotStarted,
searchTerm: '',
}
}
public render() {
const {db, source, value, notify} = this.props
const {searchTerm, isOpen} = this.state
return (
<div className={this.className} onClick={this.handleClick}>
<div className="flux-schema--item">
<div className="flex-schema-item-group">
<div className="flux-schema--expander" />
{value}
<span className="flux-schema--type">Tag Value</span>
</div>
<CopyToClipboard text={value} onCopy={this.handleCopyAttempt}>
<div className="flux-schema-copy" onClick={this.handleClickCopy}>
<span className="icon duplicate" title="copy to clipboard" />
Copy
</div>
</CopyToClipboard>
</div>
<div className={`flux-schema--children ${isOpen ? '' : 'hidden'}`}>
{this.isLoading && <LoaderSkeleton />}
{!this.isLoading && (
<>
{!!this.tags.length && (
<div className="flux-schema--filter">
<input
className="form-control input-xs"
placeholder={`Filter within ${value}`}
type="text"
spellCheck={false}
autoComplete="off"
value={searchTerm}
onClick={this.handleInputClick}
onChange={this.onSearch}
/>
</div>
)}
<TagList
db={db}
notify={notify}
source={source}
tags={this.tags}
filter={this.filter}
/>
</>
)}
</div>
</div>
)
}
private get isLoading(): boolean {
return this.state.loading === RemoteDataState.Loading
}
private get filter(): SchemaFilter[] {
const {filter, tagKey, value} = this.props
return [...filter, {key: tagKey, value}]
}
private get tags(): string[] {
const {tags, searchTerm} = this.state
const term = searchTerm.toLocaleLowerCase()
return tags.filter(t => t.toLocaleLowerCase().includes(term))
}
private async getTags() {
const {db, source} = this.props
this.setState({loading: RemoteDataState.Loading})
try {
const response = await fetchTagKeys(source, db, this.filter)
const tags = parseValuesColumn(response)
this.setState({tags, loading: RemoteDataState.Done})
} catch (error) {
console.error(error)
}
}
private get className(): string {
const {isOpen} = this.state
const openClass = isOpen ? 'expanded' : ''
return `flux-schema-tree flux-schema--child ${openClass}`
}
private handleInputClick = (e: MouseEvent<HTMLInputElement>) => {
e.stopPropagation()
}
private handleClick = (e: MouseEvent<HTMLDivElement>) => {
e.stopPropagation()
if (this.isFetchable) {
this.getTags()
}
this.setState({isOpen: !this.state.isOpen})
}
private handleClickCopy = e => {
e.stopPropagation()
}
private handleCopyAttempt = (
copiedText: string,
isSuccessful: boolean
): void => {
const {notify} = this.props
if (isSuccessful) {
notify(copyToClipboardSuccess(copiedText))
} else {
notify(copyToClipboardFailed(copiedText))
}
}
private onSearch = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({
searchTerm: e.target.value,
})
}
private get isFetchable(): boolean {
const {isOpen, loading} = this.state
return (
!isOpen &&
(loading === RemoteDataState.NotStarted ||
loading === RemoteDataState.Error)
)
}
}
export default TagValueListItem

View File

@ -1,142 +0,0 @@
import React, {PureComponent} from 'react'
import SchemaExplorer from 'src/flux/components/SchemaExplorer'
import BodyBuilder from 'src/flux/components/BodyBuilder'
import TimeMachineEditor from 'src/flux/components/TimeMachineEditor'
import Threesizer from 'src/shared/components/threesizer/Threesizer'
import {
Suggestion,
OnChangeScript,
OnSubmitScript,
OnDeleteBody,
FlatBody,
ScriptStatus,
} from 'src/types/flux'
import {Source} from 'src/types/v2'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants'
interface Props {
source: Source
script: string
body: Body[]
status: ScriptStatus
suggestions: Suggestion[]
onChangeScript: OnChangeScript
onDeleteBody: OnDeleteBody
onSubmitScript: OnSubmitScript
onAppendFrom: () => void
onAppendJoin: () => void
onValidate: () => void
}
interface Body extends FlatBody {
id: string
}
@ErrorHandling
class TimeMachine extends PureComponent<Props> {
public render() {
return (
<Threesizer
orientation={HANDLE_VERTICAL}
divisions={this.verticals}
containerClass="page-contents"
/>
)
}
private get verticals() {
return [
{
handleDisplay: 'none',
menuOptions: [],
headerButtons: [],
size: 0.33,
render: () => (
<Threesizer
divisions={this.scriptAndExplorer}
orientation={HANDLE_HORIZONTAL}
/>
),
},
this.builder,
]
}
private get builder() {
const {
body,
suggestions,
onAppendFrom,
onDeleteBody,
onAppendJoin,
} = this.props
return {
name: 'Build',
headerButtons: [],
menuOptions: [],
size: 0.67,
render: () => (
<BodyBuilder
body={body}
suggestions={suggestions}
onDeleteBody={onDeleteBody}
onAppendFrom={onAppendFrom}
onAppendJoin={onAppendJoin}
/>
),
}
}
private get scriptAndExplorer() {
const {
script,
status,
source,
onValidate,
suggestions,
onChangeScript,
onSubmitScript,
} = this.props
return [
{
name: 'Script',
handlePixels: 44,
headerOrientation: HANDLE_VERTICAL,
headerButtons: [
<div
key="validate"
className="btn btn-default btn-xs validate--button"
onClick={onValidate}
>
Validate
</div>,
],
menuOptions: [],
render: visibility => (
<TimeMachineEditor
status={status}
script={script}
visibility={visibility}
suggestions={suggestions}
onChangeScript={onChangeScript}
onSubmitScript={onSubmitScript}
/>
),
},
{
name: 'Explore',
handlePixels: 44,
headerButtons: [],
menuOptions: [],
render: () => <SchemaExplorer source={source} />,
headerOrientation: HANDLE_VERTICAL,
},
]
}
}
export default TimeMachine

View File

@ -1,27 +0,0 @@
import React from 'react'
import TimeMachineEditor from 'src/flux/components/TimeMachineEditor'
import {shallow} from 'enzyme'
const setup = (override?) => {
const props = {
script: '',
onChangeScript: () => {},
...override,
}
const wrapper = shallow(<TimeMachineEditor {...props} />)
return {
wrapper,
props,
}
}
describe('Flux.Components.TimeMachineEditor', () => {
describe('rendering', () => {
it('renders without error', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
})

View File

@ -1,213 +0,0 @@
import React, {PureComponent} from 'react'
import {Controlled as ReactCodeMirror, IInstance} from 'react-codemirror2'
import {EditorChange, LineWidget} from 'codemirror'
import {ShowHintOptions} from 'src/types/codemirror'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeScript, OnSubmitScript, Suggestion} from 'src/types/flux'
import {EXCLUDED_KEYS} from 'src/flux/constants/editor'
import {getSuggestions} from 'src/flux/helpers/autoComplete'
import 'src/external/codemirror'
interface Gutter {
line: number
text: string
}
interface Status {
type: string
text: string
}
interface Props {
script: string
visibility: string
status: Status
onChangeScript: OnChangeScript
onSubmitScript: OnSubmitScript
suggestions: Suggestion[]
}
interface Widget extends LineWidget {
node: HTMLElement
}
interface State {
lineWidgets: Widget[]
}
interface EditorInstance extends IInstance {
showHint: (options?: ShowHintOptions) => void
}
@ErrorHandling
class TimeMachineEditor extends PureComponent<Props, State> {
private editor: EditorInstance
private lineWidgets: Widget[] = []
constructor(props) {
super(props)
}
public componentDidUpdate(prevProps) {
const {status, visibility} = this.props
if (status.type === 'error') {
this.makeError()
}
if (status.type !== 'error') {
this.editor.clearGutter('error-gutter')
this.clearWidgets()
}
if (prevProps.visibility === visibility) {
return
}
if (visibility === 'visible') {
setTimeout(() => this.editor.refresh(), 60)
}
}
public render() {
const {script} = this.props
const options = {
tabIndex: 1,
mode: 'flux',
readonly: false,
lineNumbers: true,
autoRefresh: true,
theme: 'time-machine',
completeSingle: false,
gutters: ['error-gutter'],
}
return (
<div className="time-machine-editor">
<ReactCodeMirror
autoFocus={true}
autoCursor={true}
value={script}
options={options}
onBeforeChange={this.updateCode}
onTouchStart={this.onTouchStart}
editorDidMount={this.handleMount}
onBlur={this.handleBlur}
onKeyUp={this.handleKeyUp}
/>
</div>
)
}
private handleBlur = (): void => {
this.props.onSubmitScript()
}
private makeError(): void {
this.editor.clearGutter('error-gutter')
const lineNumbers = this.statusLine
lineNumbers.forEach(({line, text}) => {
const lineNumber = line - 1
this.editor.setGutterMarker(
lineNumber,
'error-gutter',
this.errorMarker(text, lineNumber)
)
})
this.editor.refresh()
}
private errorMarker(message: string, line: number): HTMLElement {
const span = document.createElement('span')
span.className = 'icon stop error-warning'
span.title = message
span.addEventListener('click', this.handleClickError(message, line))
return span
}
private handleClickError = (text: string, line: number) => () => {
let widget = this.lineWidgets.find(w => w.node.textContent === text)
if (widget) {
return this.clearWidget(widget)
}
const errorDiv = document.createElement('div')
errorDiv.className = 'inline-error-message'
errorDiv.innerText = text
widget = this.editor.addLineWidget(line, errorDiv) as Widget
this.lineWidgets = [...this.lineWidgets, widget]
}
private clearWidget = (widget: Widget): void => {
widget.clear()
this.lineWidgets = this.lineWidgets.filter(
w => w.node.textContent !== widget.node.textContent
)
}
private clearWidgets = () => {
this.lineWidgets.forEach(w => {
w.clear()
})
this.lineWidgets = []
}
private get statusLine(): Gutter[] {
const {status} = this.props
const messages = status.text.split('\n')
const lineNumbers = messages.map(text => {
const [numbers] = text.split(' ')
const [lineNumber] = numbers.split(':')
return {line: Number(lineNumber), text}
})
return lineNumbers
}
private handleMount = (instance: EditorInstance) => {
instance.refresh() // required to for proper line height on mount
this.editor = instance
}
private onTouchStart = () => {}
private handleKeyUp = (__, e: KeyboardEvent) => {
const {ctrlKey, metaKey, key} = e
if (ctrlKey && key === ' ') {
this.showAutoComplete()
return
}
if (ctrlKey || metaKey || EXCLUDED_KEYS.includes(key)) {
return
}
this.showAutoComplete()
}
private showAutoComplete() {
const {suggestions} = this.props
this.editor.showHint({
hint: () => getSuggestions(this.editor, suggestions),
completeSingle: false,
})
}
private updateCode = (
_: IInstance,
__: EditorChange,
script: string
): void => {
this.props.onChangeScript(script)
}
}
export default TimeMachineEditor

View File

@ -1,162 +0,0 @@
import React, {PureComponent, CSSProperties} from 'react'
import _ from 'lodash'
import {Grid, GridCellProps, AutoSizer, ColumnSizer} from 'react-virtualized'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {FluxTable} from 'src/types'
import {vis} from 'src/flux/constants'
const NUM_FIXED_ROWS = 1
const filterTable = (table: FluxTable): FluxTable => {
const IGNORED_COLUMNS = ['', 'result', 'table', '_start', '_stop']
const header = table.data[0]
const indices = IGNORED_COLUMNS.map(name => header.indexOf(name))
const data = table.data.map(row =>
row.filter((__, i) => !indices.includes(i))
)
return {
...table,
data,
}
}
interface Props {
table: FluxTable
}
interface State {
scrollLeft: number
filteredTable: FluxTable
}
@ErrorHandling
export default class TimeMachineTable extends PureComponent<Props, State> {
public static getDerivedStateFromProps({table}: Props) {
return {filteredTable: filterTable(table)}
}
constructor(props) {
super(props)
this.state = {
scrollLeft: 0,
filteredTable: filterTable(props.table),
}
}
public render() {
const {scrollLeft, filteredTable} = this.state
return (
<>
<AutoSizer>
{({width}) => (
<ColumnSizer
width={width}
columnCount={this.columnCount}
columnMinWidth={vis.TIME_COLUMN_WIDTH}
>
{({adjustedWidth, getColumnWidth}) => (
<Grid
className="table-graph--scroll-window"
rowCount={1}
fixedRowCount={1}
width={adjustedWidth}
scrollLeft={scrollLeft}
style={this.headerStyle}
columnWidth={getColumnWidth}
height={vis.TABLE_ROW_HEADER_HEIGHT}
columnCount={this.columnCount}
rowHeight={vis.TABLE_ROW_HEADER_HEIGHT}
cellRenderer={this.headerCellRenderer}
/>
)}
</ColumnSizer>
)}
</AutoSizer>
<AutoSizer>
{({height, width}) => (
<ColumnSizer
width={width}
columnMinWidth={vis.TIME_COLUMN_WIDTH}
columnCount={this.columnCount}
>
{({adjustedWidth, getColumnWidth}) => (
<Grid
className="table-graph--scroll-window"
width={adjustedWidth}
style={this.tableStyle}
onScroll={this.handleScroll}
columnWidth={getColumnWidth}
columnCount={this.columnCount}
cellRenderer={this.cellRenderer}
rowHeight={vis.TABLE_ROW_HEIGHT}
height={height - this.headerOffset}
rowCount={filteredTable.data.length - NUM_FIXED_ROWS}
/>
)}
</ColumnSizer>
)}
</AutoSizer>
</>
)
}
private get headerStyle(): CSSProperties {
// cannot use overflow: hidden overflow-x / overflow-y gets overridden by react-virtualized
return {overflowX: 'hidden', overflowY: 'hidden'}
}
private get tableStyle(): CSSProperties {
return {marginTop: `${this.headerOffset}px`}
}
private get columnCount(): number {
const {filteredTable} = this.state
return _.get(filteredTable, 'data.0', []).length
}
private get headerOffset(): number {
return NUM_FIXED_ROWS * vis.TABLE_ROW_HEADER_HEIGHT
}
private handleScroll = ({scrollLeft}): void => {
this.setState({scrollLeft})
}
private headerCellRenderer = ({
columnIndex,
key,
style,
}: GridCellProps): React.ReactNode => {
const {filteredTable} = this.state
return (
<div
key={key}
style={{...style, display: 'flex', alignItems: 'center'}}
className="table-graph-cell table-graph-cell__fixed-row"
>
{filteredTable.data[0][columnIndex]}
</div>
)
}
private cellRenderer = ({
columnIndex,
key,
rowIndex,
style,
}: GridCellProps): React.ReactNode => {
const {filteredTable} = this.state
return (
<div key={key} style={style} className="table-graph-cell">
{filteredTable.data[rowIndex + NUM_FIXED_ROWS][columnIndex]}
</div>
)
}
}

View File

@ -1,137 +0,0 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {FluxTable} from 'src/types'
import TableSidebar from 'src/flux/components/TableSidebar'
import TimeMachineTable from 'src/flux/components/TimeMachineTable'
import FluxGraph from 'src/flux/components/FluxGraph'
import NoResults from 'src/flux/components/NoResults'
import {Radio} from 'src/clockface'
interface Props {
data: FluxTable[]
yieldName: string
}
enum VisType {
Table = 'Table View',
Line = 'Line Graph',
}
interface State {
selectedResultID: string | null
visType: VisType
}
@ErrorHandling
class TimeMachineVis extends PureComponent<Props, State> {
constructor(props) {
super(props)
this.state = {
selectedResultID: this.initialResultID,
visType: VisType.Table,
}
}
public componentDidUpdate() {
if (!this.selectedResult) {
this.setState({selectedResultID: this.initialResultID})
}
}
public render() {
const {yieldName} = this.props
const {visType} = this.state
return (
<>
<div className="yield-node--controls">
<Radio>
<Radio.Button
id="vis-type--table"
active={visType === VisType.Table}
value={VisType.Table}
onClick={this.selectVisType}
titleText="View results in a Table"
>
Table
</Radio.Button>
<Radio.Button
id="vis-type--line"
active={visType === VisType.Line}
value={VisType.Line}
onClick={this.selectVisType}
titleText="View results on a Line Graph"
>
Line
</Radio.Button>
</Radio>
<div className="yield-node--name">{`"${yieldName}"`}</div>
</div>
<div className="yield-node--visualization">{this.vis}</div>
</>
)
}
private get vis(): JSX.Element {
const {visType} = this.state
const {data} = this.props
if (visType === VisType.Line) {
return <FluxGraph data={data} />
}
return this.table
}
private get table(): JSX.Element {
return (
<>
{this.showSidebar && (
<TableSidebar
data={this.props.data}
selectedResultID={this.state.selectedResultID}
onSelectResult={this.handleSelectResult}
/>
)}
<div className="yield-node--table">
{this.shouldShowTable && (
<TimeMachineTable table={this.selectedResult} />
)}
{!this.hasResults && <NoResults />}
</div>
</>
)
}
private get initialResultID(): string {
return _.get(this.props.data, '0.id', null)
}
private handleSelectResult = (selectedResultID: string): void => {
this.setState({selectedResultID})
}
private selectVisType = (visType: VisType): void => {
this.setState({visType})
}
private get showSidebar(): boolean {
return this.props.data.length > 1
}
private get hasResults(): boolean {
return !!this.props.data.length
}
private get shouldShowTable(): boolean {
return !!this.props.data && !!this.selectedResult
}
private get selectedResult(): FluxTable {
return this.props.data.find(d => d.id === this.state.selectedResultID)
}
}
export default TimeMachineVis

View File

@ -1,67 +0,0 @@
import React, {PureComponent} from 'react'
interface Props {
name: string
assignedToQuery: boolean
}
interface State {
isExpanded: boolean
}
export default class VariableName extends PureComponent<Props, State> {
public static defaultProps: Partial<Props> = {
assignedToQuery: false,
}
constructor(props) {
super(props)
this.state = {
isExpanded: false,
}
}
public render() {
const {assignedToQuery} = this.props
return (
<div className="variable-node">
{assignedToQuery && <div className="variable-node--connector" />}
{this.nameElement}
</div>
)
}
private get nameElement(): JSX.Element {
const {name} = this.props
if (name.includes('=')) {
return this.colorizeSyntax
}
return <span className="variable-node--name">{name}</span>
}
private get colorizeSyntax(): JSX.Element {
const {name} = this.props
const split = name.split('=')
const varName = split[0].substring(0, split[0].length - 1)
const varValue = this.props.name.replace(/^[^=]+=/, '')
const valueIsString = varValue.endsWith('"')
return (
<>
<span className="variable-node--name">{varName}</span>
{' = '}
<span
className={
valueIsString ? 'variable-node--string' : 'variable-node--number'
}
>
{varValue}
</span>
</>
)
}
}

View File

@ -1,69 +0,0 @@
import React, {PureComponent} from 'react'
import _ from 'lodash'
import {ErrorHandling} from 'src/shared/decorators/errors'
import TimeMachineVis from 'src/flux/components/TimeMachineVis'
import {getTimeSeries} from 'src/flux/apis'
import {getDeep} from 'src/utils/wrappers'
import {FluxTable} from 'src/types'
import {Func} from 'src/types/flux'
import {Source} from 'src/types/v2'
interface Props {
source: Source
data: FluxTable[]
index: number
bodyID: string
func: Func
declarationID?: string
script: string
}
interface State {
data: FluxTable[]
}
@ErrorHandling
class YieldFuncNode extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
data: [],
}
}
public componentDidMount() {
this.getData()
}
public componentDidUpdate(prevProps: Props) {
if (prevProps.script !== this.props.script) {
this.getData()
}
}
public render() {
const {func} = this.props
const {data} = this.state
const yieldName = _.get(func, 'args.0.value', 'result')
return (
<div className="yield-node">
<div className="func-node--connector" />
<TimeMachineVis data={data} yieldName={yieldName} />
</div>
)
}
private getData = async (): Promise<void> => {
const {script, source} = this.props
const results = await getTimeSeries(source.links.query, script)
const data = getDeep<FluxTable[]>(results, 'tables', [])
this.setState({data})
}
}
export default YieldFuncNode

View File

@ -1,21 +0,0 @@
/*
Styles for Flux Builder aka TIME MACHINE aka DELOREAN
----------------------------------------------------------------------------
*/
// Flux Page Empty state
.flux-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%,-50%);
display: inline-flex;
flex-direction: column;
align-items: center;
> p {
color: $g11-sidewalk;
font-size: 16px;
@include no-user-select();
}
}

View File

@ -1,100 +0,0 @@
/*
Flux Add Function Button
----------------------------------------------------------------------------
*/
$flux-func-selector--gap: 10px;
$flux-func-selector--height: 30px;
.flux-func--selector {
display: flex;
align-items: center;
position: relative;
flex-direction: column;
&.open {
z-index: 9999;
height: $flux-func-selector--height + $flux-func-selector--gap;
}
}
.func-selector--connector {
width: $flux-node-gap;
height: $flux-func-selector--gap;
position: relative;
&:after {
content: '';
position: absolute;
top: -130%;
width: $flux-connector-line;
left: 50%;
height: 230%;
transform: translateX(-50%);
@include gradient-v($g4-onyx, $c-pool);
}
}
.btn.btn-sm.flux-func--button {
border-radius: 50%;
float: left;
&:focus {
box-shadow: 0 0 8px 3px $c-amethyst;
}
}
.flux-func--autocomplete,
.flux-func--list {
position: absolute;
width: 166px;
}
.flux-func--autocomplete {
left: 0;
top: 0;
.func-selector--connector + & {
top: $flux-func-selector--gap;
}
}
.flux-func--list {
left: 0;
border-radius: $radius;
top: $flux-func-selector--height;
padding: 0;
margin: 0;
@extend %no-user-select;
@include gradient-h($c-star, $c-pool);
}
.flux-func--item {
height: 28px;
line-height: 28px;
padding: 0 11px;
margin: 0;
font-size: 13px;
color: $c-neutrino;
&:first-child {
border-radius: 4px 4px 0 0;
}
&:last-child {
border-radius: 0 0 4px 4px;
}
&.active {
@include gradient-h($c-comet, $c-laser);
color: $g20-white;
}
&:hover {
cursor: pointer;
}
&.empty {
font-style: italic;
color: $c-hydrogen;
}
}

View File

@ -1,527 +0,0 @@
/*
Flux Builder Styles
------------------------------------------------------------------------------
*/
$flux-builder-min-width: 440px;
$flux-node-height: 30px;
$flux-node-tooltip-gap: 4px;
$flux-connector-line: 2px;
$flux-node-gap: 30px;
$flux-node-padding: 10px;
$flux-arg-min-width: 120px;
$flux-func-color: $c-comet;
$flux-number-color: $c-hydrogen;
$flux-object-color: $c-viridian;
$flux-string-color: $c-honeydew;
$flux-boolean-color: $c-viridian;
$flux-invalid-color: $c-curacao;
$flux-func-hover: $c-moonstone;
$flux-number-hover: $c-neutrino;
$flux-object-hover: $c-rainforest;
$flux-string-hover: $c-wasabi;
$flux-boolean-hover: $c-rainforest;
$flux-invalid-hover: $c-dreamsicle;
// Shared Node styles
%flux-node {
min-height: $flux-node-height;
border-radius: $radius;
padding: 0 $flux-node-padding;
font-size: 13px;
font-weight: 600;
position: relative;
background-color: $g4-onyx;
transition: background-color 0.25s ease;
margin-bottom: $flux-node-tooltip-gap / 2;
margin-top: $flux-node-tooltip-gap / 2;
&:hover {
cursor: pointer;
background-color: $g6-smoke;
}
}
.body-builder--container {
background-color: $g1-raven;
}
.body-builder {
padding: $flux-node-height;
padding-bottom: 0;
min-width: $flux-builder-min-width;
width: 100%;
}
.declaration {
width: 100%;
margin-bottom: $flux-node-gap;
padding-bottom: $flux-node-gap;
border-bottom: 2px solid $g2-kevlar;
display: flex;
flex-wrap: nowrap;
flex-direction: column;
align-items: flex-start;
&:last-of-type {
margin-bottom: 0;
border: 0;
}
}
.variable-node {
@extend %flux-node;
color: $g11-sidewalk;
line-height: $flux-node-height;
white-space: nowrap;
@include no-user-select();
margin-top: 0;
&:hover {
background-color: $g4-onyx;
cursor: default;
}
}
.variable-node--connector {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $c-pool;
position: absolute;
z-index: 2;
top: 50%;
left: $flux-node-gap / 2;
transform: translate(-50%, -50%);
&:before {
content: '';
position: absolute;
top: 100%;
left: 50%;
width: $flux-connector-line;
height: $flux-node-gap;
@include gradient-v($c-pool, $g4-onyx);
transform: translateX(-50%);
}
}
.variable-node--name {
color: $c-pool;
.variable-node--connector+& {
margin-left: $flux-node-gap - $flux-node-padding;
}
}
.variable-node--string,
.func-arg--string {
color: $flux-string-color;
}
.variable-node--boolean,
.func-arg--boolean {
color: $flux-boolean-color;
}
.variable-node--number,
.func-arg--number {
color: $flux-number-color;
}
.variable-node--object,
.func-arg--object {
color: $flux-object-color;
}
.variable-node--invalid,
.func-arg--invalid {
color: $flux-invalid-color;
}
.func-node {
@extend %flux-node;
display: flex;
align-items: center;
margin-left: $flux-node-gap;
&:hover,
&.active {
cursor: pointer !important;
.func-node--preview {
color: $g20-white;
}
.func-arg--string {
color: $flux-string-hover;
}
.func-arg--boolean {
color: $flux-boolean-hover;
}
.func-arg--number {
color: $flux-number-hover;
}
.func-arg--invalid {
color: $flux-invalid-hover;
}
}
}
.func-node--connector {
width: $flux-node-gap;
height: 100%;
position: absolute;
top: 0;
left: 0;
transform: translateX(-100%);
z-index: 0;
pointer-events: none;
// Connection Lines
&:before,
&:after {
content: '';
background-color: $g4-onyx;
position: absolute;
} // Vertical Line
&:before {
width: $flux-connector-line;
height: calc(100% + #{$flux-node-tooltip-gap});
top: -$flux-node-tooltip-gap / 2;
left: $flux-node-gap / 2;
transform: translateX(-50%);
} // Horizontal Line
&:after {
height: $flux-connector-line;
width: $flux-node-gap / 2;
top: 50%;
left: $flux-node-gap / 2;
transform: translateY(-50%);
}
}
// When a query exists unassigned to a variable
.func-node--wrapper:first-child .func-node {
margin-left: 0;
padding-left: $flux-node-gap;
.func-node--connector {
transform: translateX(0);
z-index: 2; // Vertical Line
&:before {
height: $flux-node-gap;
top: $flux-node-gap / 2;
@include gradient-v($c-comet, $g4-onyx);
} // Dot
&:after {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: $c-comet;
top: $flux-node-gap / 2;
transform: translate(-50%, -50%);
}
}
}
.func-node--name,
.func-node--preview {
font-size: 13px;
@include no-user-select();
white-space: nowrap;
transition: color 0.25s ease;
font-weight: 600;
}
.func-node--name {
height: $flux-node-height;
line-height: $flux-node-height;
color: $flux-func-color;
.func-node:hover &,
.func-node.active & {
color: $flux-func-hover;
}
}
.func-node--preview {
color: $g11-sidewalk;
margin-left: 4px;
padding: 5px 0;
display: flex;
align-items: center;
.func-node:hover & {
color: $g17-whisper;
}
}
.func-node--wrapper {
display: flex;
align-items: center;
}
.func-node--menu {
display: flex;
align-items: center;
opacity: 0;
transition: opacity 0.25s ease;
> button.btn,
> .confirm-button {
margin-left: 4px;
}
}
.func-node--wrapper:hover .func-node--menu,
.func-node.editing + .func-node--menu,
.func-node.active + .func-node--menu {
opacity: 1;
}
.func-node--editor {
position: relative;
margin-left: $flux-node-gap;
margin-bottom: $flux-node-gap;
margin-top: $flux-node-tooltip-gap / 2;
background-color: $g3-castle;
border-radius: $radius;
padding: 6px;
display: flex;
align-items: stretch;
}
.func-node--editor .func-node--connector {
// Vertical Line
&:before {
height: calc(100% + #{($flux-node-tooltip-gap / 2) + $flux-node-gap});
}
// Horizontal Line
&:after {
content: none;
}
}
.func-arg--buttons {
display: flex;
flex-direction: column;
justify-content: center;
}
.func-node--build {
width: 60px;
margin-top: 4px;
}
.func-args {
display: flex;
flex-direction: column;
}
.func-arg {
min-width: $flux-arg-min-width;
display: flex;
flex-wrap: nowrap;
align-items: center;
margin-bottom: 4px;
&:last-of-type {
margin-bottom: 0;
}
}
.func-arg--label {
white-space: nowrap;
font-size: 13px;
font-weight: 600;
color: $g10-wolf;
padding: 0 8px;
@include no-user-select();
}
.func-arg--value {
flex: 1 0 0;
}
.func-arg--textarea {
overflow: hidden;
width: 300px;
}
/*
Filter Preview Styles
------------------------------------------------------------------------------
*/
$flux-filter-gap: 6px;
$flux-filter-unit: 26px;
$flux-filter-unit-wrapped: 34px;
$flux-filter-expression: $g3-castle;
$flux-filter-parens: $g5-pepper;
%flux-filter-style {
height: $flux-filter-unit;
line-height: $flux-filter-unit;
border-style: solid;
border-width: 0;
transition: background-color 0.25s ease;
}
.flux-filter--key {
@extend %flux-filter-style;
background-color: $flux-filter-expression;
border-radius: 3px 0 0 3px;
padding-left: $flux-filter-gap;
}
.flux-filter--operator {
@extend %flux-filter-style;
text-transform: uppercase;
padding: 0 ($flux-filter-gap / 2);
}
.flux-filter--value+.flux-filter--operator,
.flux-filter--paren-close+.flux-filter--operator {
padding: 0 $flux-filter-gap;
}
.flux-filter--key+.flux-filter--operator {
background-color: $flux-filter-expression;
}
.flux-filter--key+.flux-filter--operator+.flux-filter--value {
background-color: $flux-filter-expression;
border-radius: 0 3px 3px 0;
}
.flux-filter--value {
@extend %flux-filter-style;
padding-right: $flux-filter-gap;
&.number {
color: $flux-number-color;
}
&.string {
color: $flux-string-color;
}
&.boolean {
color: $flux-boolean-color;
}
}
.flux-filter--paren-open,
.flux-filter--paren-close {
@extend %flux-filter-style;
height: $flux-filter-unit-wrapped;
width: ($flux-filter-unit-wrapped - $flux-filter-unit) / 2;
background-color: $flux-filter-parens;
border: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) solid
$flux-filter-expression;
transition: border-color 0.25s ease;
}
.flux-filter--paren-open {
border-right: 0;
border-radius: 3px 0 0 3px;
}
.flux-filter--paren-close {
border-left: 0;
border-radius: 0 3px 3px 0;
}
%flux-filter-wrapped {
position: relative;
z-index: 2;
background-color: $flux-filter-parens;
transition: background-color 0.25s ease;
&:before {
content: '';
width: 100%;
height: $flux-filter-unit-wrapped;
position: absolute;
top: ($flux-filter-unit - $flux-filter-unit-wrapped) / 2;
left: 0;
border-style: solid;
border-width: (($flux-filter-unit-wrapped - $flux-filter-unit) / 2) 0;
border-color: $flux-filter-expression;
z-index: -1;
box-sizing: border-box;
transition: border-color 0.25s ease;
}
}
.flux-filter--paren-open+.flux-filter--key,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value {
@extend %flux-filter-wrapped;
}
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator {
background-color: $flux-filter-expression;
height: $flux-filter-unit-wrapped;
line-height: $flux-filter-unit-wrapped;
}
.flux-filter--fancyscroll {
min-width: 300px;
min-height: 250px;
}
.flux-filter--helper-text {
@include no-user-select();
color: $g13-mist;
font-size: 12px;
font-weight: 500;
padding-left: 20px;
}
// Func Node Active State (When node is before a yield)
.func-node.active {
background-color: $c-star;
.func-node--connector:after {
@include gradient-h($g3-castle, $c-star);
}
.flux-filter--value.number {
color: $flux-number-hover;
}
.flux-filter--value.string {
color: $flux-string-hover;
}
.flux-filter--value.boolean {
color: $flux-boolean-hover;
}
.flux-filter--key,
.flux-filter--key + .flux-filter--operator,
.flux-filter--key + .flux-filter--operator + .flux-filter--value {
background-color: $c-amethyst;
}
.flux-filter--paren-open,
.flux-filter--paren-close {
border-color: $c-amethyst;
}
.flux-filter--paren-open+.flux-filter--key,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value {
background-color: $c-star;
&:before {
border-color: $c-amethyst;
}
}
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator,
.flux-filter--paren-open+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator+.flux-filter--key+.flux-filter--operator+.flux-filter--value+.flux-filter--operator {
background-color: $c-amethyst;
}
}

View File

@ -1,31 +0,0 @@
/*
Flux Code Mirror Editor
----------------------------------------------------------------------------
*/
.time-machine-container {
display: flex;
height: 90%;
justify-content: baseline;
align-items: stretch;
}
.func-nodes-container,
.time-machine-editor-container {
flex: 1 0 50%;
}
.time-machine-editor {
width: 100%;
height: 100%;
}
.error-warning {
color: $c-dreamsicle;
cursor: pointer;
}
.inline-error-message {
color: white;
background-color: red;
}

View File

@ -1,330 +0,0 @@
/*
Flux Schema Explorer -- Tree View
----------------------------------------------------------------------------
*/
$flux-tree-min-width: 250px;
$flux-tree-indent: 26px;
$flux-tree-line: 2px;
$flux-tree-max-filter: 220px;
$flux-tree-gutter: 11px;
.flux-schema-explorer {
width: 100%;
height: 100%;
background-color: $g2-kevlar;
min-width: $flux-tree-min-width;
padding-left: 30px;
}
.flux-schema-tree {
position: relative;
display: flex;
flex-direction: column;
align-items: stretch;
padding-left: 0;
> .flux-schema--children > .flux-schema-tree {
padding-left: $flux-tree-indent;
}
}
.flux-schema--children.hidden {
display: none;
}
.flux-schema-tree__empty {
height: $flux-tree-indent;
display: flex;
align-items: center;
padding: 0 $flux-tree-gutter;
font-size: 12px;
font-weight: 600;
color: $g8-storm;
font-style: italic;
}
.flux-schema--expander {
width: $flux-tree-indent;
height: $flux-tree-indent;
position: relative; // Plus Sign
&:before,
&:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: $g11-sidewalk;
width: $flux-tree-indent / 3;
height: $flux-tree-line;
transition: transform 0.25s ease, background-color 0.25s ease;
} // Vertical Line
&:after {
transform: translate(-50%, -50%) rotate(90deg);
}
}
.flux-schema--item {
@include no-user-select();
position: relative;
height: $flux-tree-indent;
display: flex;
align-items: center;
padding: 0 $flux-tree-gutter;
padding-left: 0;
font-size: 12px;
font-weight: 600;
color: $g11-sidewalk;
white-space: nowrap;
transition: color 0.25s ease, background-color 0.25s ease;
> span.icon {
position: absolute;
top: 50%;
left: $flux-tree-indent / 2;
transform: translate(-50%, -50%);
}
&:not(.no-hover):hover {
color: $g17-whisper;
cursor: pointer;
background-color: $g4-onyx;
.flux-schema--expander:before,
.flux-schema--expander:after {
background-color: $g17-whisper;
}
}
.expanded > & {
color: $c-pool;
.flux-schema--expander:before,
.flux-schema--expander:after {
background-color: $c-pool;
}
.flux-schema--expander:before {
transform: translate(-50%, -50%) rotate(-90deg);
width: $flux-tree-line;
}
.flux-schema--expander:after {
transform: translate(-50%, -50%) rotate(0deg);
}
&:hover {
color: $c-laser;
.flux-schema--expander:before,
.flux-schema--expander:after {
background-color: $c-laser;
}
}
}
&.readonly,
&.readonly:hover {
padding-left: $flux-tree-indent + 8px;
background-color: transparent;
color: $g11-sidewalk;
cursor: default;
}
.increase-values-limit {
margin-left: 8px;
padding: 0 $flux-tree-gutter;
}
.no-results {
margin-left: 8px;
font-style: italic;
color: $g9-mountain;
}
}
.flux-schema--child .flux-schema--item {
border-radius: 4px 0 0 4px;
}
.flex-schema-item-group {
display: flex;
flex: 1 0 0;
align-items: center;
}
.flux-schema-copy {
color: $g11-sidewalk;
display: inline-block;
opacity: 0;
transition: opacity 0.25s ease;
margin-left: 8px;
.flux-schema--item:hover & {
opacity: 1;
}
> span.icon {
margin-right: 3px;
}
&:hover {
color: $g15-platinum;
}
}
@keyframes skeleton-animation {
0% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.flux-schema--item-skeleton {
background: linear-gradient(60deg, $g4-onyx 0%, $g4-onyx 35%, $g6-smoke 50%, $g4-onyx 65%, $g4-onyx 100%);
border-radius: 4px;
height: 60%;
background-size: 400% 400%;
animation: skeleton-animation 1s ease infinite;
}
// Tree Node Lines
.flux-schema--child:before,
.flux-schema--child:after {
content: '';
background-color: $g4-onyx;
position: absolute;
}
// Vertical Line
.flux-schema--child:before {
top: 0;
left: $flux-tree-indent / 2;
width: $flux-tree-line;
height: 100%;
}
.flux-schema--child:last-child:before {
height: $flux-tree-indent / 2;
}
// Horizontal Line
.flux-schema--child:after {
top: $flux-tree-indent / 2;
left: $flux-tree-indent / 2;
width: $flux-tree-indent / 2;
height: $flux-tree-line;
}
/*
Controls
----------------------------------------------------------------------------
*/
.flux-schema--controls {
padding: $flux-tree-gutter;
display: flex;
align-items: center;
justify-content: space-between;
}
.flux-schema--filter {
display: flex;
position: relative;
padding-left: $flux-tree-indent;
padding-right: $flux-tree-gutter;
align-items: center;
height: 30px;
&:before,
&:after {
content: '';
background-color: $g4-onyx;
position: absolute;
}
// Vertical Line
&:before {
top: 0;
left: $flux-tree-indent / 2;
width: $flux-tree-line;
height: 100%;
}
// Horizontal Line
&:after {
top: $flux-tree-indent / 2;
left: $flux-tree-indent / 2;
width: $flux-tree-indent / 2;
height: $flux-tree-line;
}
& > input.form-control.input-xs {
max-width: $flux-tree-max-filter;
font-size: 12px;
}
}
// Hints
.flux-schema--type {
color: $g11-sidewalk;
display: inline-block;
margin-left: 8px;
opacity: 0;
transition: opacity 0.25s ease;
.flux-schema--item:hover & {
opacity: 1;
}
}
.flux-schema--header {
display: flex;
align-items: center;
padding-right: 11px;
> .flux-schema--filter {
flex: 1 0 0;
max-width: $flux-tree-max-filter + $flux-tree-indent + $flux-tree-gutter;
}
}
.flux-schema--count {
font-size: 12px;
color: $g11-sidewalk;
font-weight: 600;
height: 22px;
line-height: 22px;
border-radius: 11px;
background-color: $g4-onyx;
padding: 0 8px;
@include no-user-select();
}
/*
Spinner
----------------------------------------------------------------------------
From http: //tobiasahlin.com/spinkit/.
*/
.loading-spinner .spinner {
width: 25px;
text-align: center;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: nowrap;
}
.loading-spinner .spinner > div {
width: 8px;
height: 8px;
background-color: $c-pool;
border-radius: 50%;
animation: sk-bouncedelay 1s infinite ease-in-out both;
}
.loading-spinner .spinner .bounce1 {
animation-delay: -0.32s;
}
.loading-spinner .spinner .bounce2 {
animation-delay: -0.16s;
}
@keyframes sk-bouncedelay {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}

View File

@ -1,4 +0,0 @@
.flux-overlay {
max-width: 500px;
margin: 0 auto;
}

View File

@ -1,153 +0,0 @@
/*
Yield Node Styles
------------------------------------------------------------------------------
*/
$flux-builder-vis-height: 550px;
$flux-builder-yield-tabs-min-width: 180px;
$flux-builder-yield-tabs-max-width: 400px;
.yield-node {
position: relative;
display: flex;
flex-direction: column;
width: calc(100% - #{$flux-node-gap});
height: $flux-builder-vis-height;
margin-bottom: $flux-node-tooltip-gap / 2;
margin-top: $flux-node-tooltip-gap / 2;
margin-left: $flux-node-gap;
// Hide Horizontal Line
& > .func-node--connector:after {
content: none;
}
}
.yield-node--controls {
background-color: $g3-castle;
padding: $flux-node-padding;
border-radius: $radius $radius 0 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.yield-node--name {
font-size: 12px;
font-weight: 600;
margin-right: 6px;
@include no-user-select();
color: $c-honeydew;
}
.yield-node--visualization {
display: flex;
align-items: stretch;
flex-wrap: none;
width: 100%;
height: 100%;
}
// Sidebar
.yield-node--sidebar {
display: flex;
flex-direction: column;
align-items: stretch;
flex-wrap: nowrap;
width: 25%;
min-width: $flux-builder-yield-tabs-min-width;
max-width: $flux-builder-yield-tabs-max-width;
background-color: $g2-kevlar;
overflow: hidden;
border-radius: $radius 0 0 $radius;
}
.yield-node--sidebar-heading {
padding: $flux-node-padding;
}
.yield-node--sidebar-filter.form-control.input-xs {
font-size: 12px;
}
// Tabs
.yield-node--tabs {
display: flex;
flex-direction: column;
position: relative;
// Shadow
&:before {
content: '';
position: absolute;
top: 0;
right: 0;
width: $flux-node-padding;
height: 100%;
@include gradient-h(fade-out($g2-kevlar, 1), fade-out($g2-kevlar, 0.4));
pointer-events: none;
}
}
.yield-node--tab {
@include no-user-select();
color: $g11-sidewalk;
height: 28px;
display: flex;
align-items: center;
font-size: 12px;
font-weight: 600;
padding: 0 $flux-node-padding;
transition: color 0.25s ease, background-color 0.25s ease;
white-space: nowrap;
overflow-x: hidden;
&:hover {
background-color: $g4-onyx;
color: $g15-platinum;
cursor: pointer;
}
&.active {
background-color: $g5-pepper;
color: $g18-cloud;
}
> span {
padding-right: 1px;
padding-left: 1px;
}
> span.key {
color: $g9-mountain;
}
> span.value {
padding-right: 5px;
color: $g11-sidewalk;
}
}
// Table
.yield-node--table {
border-left: 1px solid $g5-pepper;
flex: 1 0 0;
background-color: $g3-castle;
overflow: hidden;
border-radius: 0 0 $radius $radius;
&:only-child {
border: 0;
}
}
// Line Graph
.yield-node--graph {
position: relative;
width: 100%;
height: 100%;
padding: 8px 16px;
background-color: $g3-castle;
border-radius: 0 0 $radius $radius;
}

View File

@ -1,27 +0,0 @@
import React from 'react'
import FluxEditor from 'src/flux/components/TimeMachineEditor'
import {shallow} from 'enzyme'
const setup = (override?) => {
const props = {
script: '',
onChangeScript: () => {},
...override,
}
const wrapper = shallow(<FluxEditor {...props} />)
return {
wrapper,
props,
}
}
describe('Flux.Components.FluxEditor', () => {
describe('rendering', () => {
it('renders without error', () => {
const {wrapper} = setup()
expect(wrapper.exists()).toBe(true)
})
})
})

View File

@ -1,13 +1,22 @@
// Libraries
import React, {PureComponent} from 'react'
import {Controlled as ReactCodeMirror, IInstance} from 'react-codemirror2'
import {EditorChange, LineWidget} from 'codemirror'
import {ShowHintOptions} from 'src/types/codemirror'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {OnChangeScript, OnSubmitScript, Suggestion} from 'src/types/flux'
import {EXCLUDED_KEYS} from 'src/flux/constants/editor'
import {getSuggestions} from 'src/flux/helpers/autoComplete'
import 'src/external/codemirror'
// Components
import {ErrorHandling} from 'src/shared/decorators/errors'
// Constants
import {EXCLUDED_KEYS} from 'src/flux/constants/editor'
// Utils
import {getSuggestions} from 'src/flux/helpers/autoComplete'
// Types
import {OnChangeScript, OnSubmitScript, Suggestion} from 'src/types/flux'
interface Gutter {
line: number
text: string

View File

@ -1,13 +0,0 @@
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

@ -1,171 +0,0 @@
export const emptyAST = {
type: 'Program',
location: {
start: {
line: 1,
column: 1,
},
end: {
line: 1,
column: 1,
},
source: '',
},
body: [],
}
export const ast = {
type: 'File',
start: 0,
end: 22,
loc: {
start: {
line: 1,
column: 0,
},
end: {
line: 1,
column: 22,
},
},
program: {
type: 'Program',
start: 0,
end: 22,
loc: {
start: {
line: 1,
column: 0,
},
end: {
line: 1,
column: 22,
},
},
sourceType: 'module',
body: [
{
type: 'ExpressionStatement',
start: 0,
end: 22,
loc: {
start: {
line: 1,
column: 0,
},
end: {
line: 1,
column: 22,
},
},
expression: {
type: 'CallExpression',
start: 0,
end: 22,
loc: {
start: {
line: 1,
column: 0,
},
end: {
line: 1,
column: 22,
},
},
callee: {
type: 'Identifier',
start: 0,
end: 4,
loc: {
start: {
line: 1,
column: 0,
},
end: {
line: 1,
column: 4,
},
identifierName: 'from',
},
name: 'from',
},
arguments: [
{
type: 'ObjectExpression',
start: 5,
end: 21,
loc: {
start: {
line: 1,
column: 5,
},
end: {
line: 1,
column: 21,
},
},
properties: [
{
type: 'ObjectProperty',
start: 6,
end: 20,
loc: {
start: {
line: 1,
column: 6,
},
end: {
line: 1,
column: 20,
},
},
method: false,
shorthand: false,
computed: false,
key: {
type: 'Identifier',
start: 6,
end: 8,
loc: {
start: {
line: 1,
column: 6,
},
end: {
line: 1,
column: 8,
},
identifierName: 'db',
},
name: 'db',
},
value: {
type: 'StringLiteral',
start: 10,
end: 20,
loc: {
start: {
line: 1,
column: 10,
},
end: {
line: 1,
column: 20,
},
},
extra: {
rawValue: 'telegraf',
raw: 'telegraf',
},
value: 'telegraf',
},
},
],
},
],
},
},
],
directives: [],
},
}

View File

@ -1,2 +0,0 @@
export const NEW_FROM = `from(db: "pick a db")\n\t|> filter(fn: (r) => r.tag == "value")\n\t|> range(start: -1m)`
export const NEW_JOIN = `join(tables:{fil:fil, tele:tele}, on:["host"], fn:(tables) => tables.fil["_value"] + tables.tele["_value"])`

View File

@ -1,7 +0,0 @@
export enum FluxFormMode {
NEW = 'new',
EDIT = 'edit',
}
export const FLUX_CONNECTION_TOOLTIP =
'<p>Flux Connections are<br/>scoped per InfluxDB Connection.<br/>Only one can be active at a time.</p>'

View File

@ -1 +0,0 @@
export const TAG_VALUES_LIMIT = 10

View File

@ -1,3 +0,0 @@
export const FROM = 'from'
export const FILTER = 'filter'
export const JOIN = 'join'

View File

@ -1,21 +1 @@
import {ast, emptyAST} from 'src/flux/constants/ast'
import * as editor from 'src/flux/constants/editor'
import * as argTypes from 'src/flux/constants/argumentTypes'
import * as funcNames from 'src/flux/constants/funcNames'
import * as builder from 'src/flux/constants/builder'
import * as vis from 'src/flux/constants/vis'
import * as explorer from 'src/flux/constants/explorer'
const MAX_RESPONSE_BYTES = 1e7 // 10 MB
export {
ast,
emptyAST,
funcNames,
argTypes,
editor,
builder,
vis,
explorer,
MAX_RESPONSE_BYTES,
}
export const MAX_RESPONSE_BYTES = 1e7 // 10 MB

View File

@ -1,3 +0,0 @@
export const TABLE_ROW_HEADER_HEIGHT = 40
export const TABLE_ROW_HEIGHT = 30
export const TIME_COLUMN_WIDTH = 170

View File

@ -1,697 +0,0 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
/// Components
import TimeMachine from 'src/flux/components/TimeMachine'
import KeyboardShortcuts from 'src/shared/components/KeyboardShortcuts'
import {Page} from 'src/pageLayout'
// APIs
import {getSuggestions, getAST, getTimeSeries} from 'src/flux/apis'
// Constants
import {
validateSuccess,
fluxTimeSeriesError,
fluxResponseTruncatedError,
} from 'src/shared/copy/notifications'
import {builder, argTypes, emptyAST} from 'src/flux/constants'
// Actions
import {
UpdateScript,
updateScript as updateScriptAction,
} from 'src/flux/actions'
import {notify as notifyAction} from 'src/shared/actions/notifications'
// Utils
import {bodyNodes} from 'src/flux/helpers'
// Types
import {Source} from 'src/types/v2'
import {Notification, FluxTable} from 'src/types'
import {
Suggestion,
FlatBody,
Links,
InputArg,
Context,
DeleteFuncNodeArgs,
Func,
ScriptStatus,
} from 'src/types/flux'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Status {
type: string
text: string
}
interface Props {
links: Links
source: Source
notify: (message: Notification) => void
script: string
updateScript: UpdateScript
}
interface Body extends FlatBody {
id: string
}
interface State {
body: Body[]
ast: object
data: FluxTable[]
status: ScriptStatus
suggestions: Suggestion[]
}
type ScriptFunc = (script: string) => void
export const FluxContext = React.createContext({})
@ErrorHandling
export class FluxPage extends PureComponent<Props, State> {
private debouncedASTResponse: ScriptFunc
constructor(props) {
super(props)
this.state = {
body: [],
ast: null,
data: [],
suggestions: [],
status: {
type: 'none',
text: '',
},
}
this.debouncedASTResponse = _.debounce(script => {
this.getASTResponse(script, false)
}, 250)
}
public async componentDidMount() {
const {links} = this.props
try {
const suggestions = await getSuggestions(links.suggestions)
this.setState({suggestions})
} catch (error) {
console.error('Could not get function suggestions: ', error)
}
this.getTimeSeries()
}
public render() {
const {suggestions, body, status} = this.state
const {script, source} = this.props
return (
<FluxContext.Provider value={this.getContext}>
<KeyboardShortcuts onControlEnter={this.getTimeSeries}>
<Page>
<Page.Header fullWidth={true}>
<Page.Header.Left>
<Page.Title title="Flux Editor" />
</Page.Header.Left>
<Page.Header.Right />
</Page.Header>
<TimeMachine
body={body}
script={script}
status={status}
source={source}
suggestions={suggestions}
onValidate={this.handleValidate}
onAppendFrom={this.handleAppendFrom}
onAppendJoin={this.handleAppendJoin}
onChangeScript={this.handleChangeScript}
onSubmitScript={this.handleSubmitScript}
onDeleteBody={this.handleDeleteBody}
/>
</Page>
</KeyboardShortcuts>
</FluxContext.Provider>
)
}
private get getContext(): Context {
return {
onAddNode: this.handleAddNode,
onChangeArg: this.handleChangeArg,
onSubmitScript: this.handleSubmitScript,
onChangeScript: this.handleChangeScript,
onDeleteFuncNode: this.handleDeleteFuncNode,
onGenerateScript: this.handleGenerateScript,
onToggleYield: this.handleToggleYield,
data: this.state.data,
scriptUpToYield: this.handleScriptUpToYield,
source: this.props.source,
}
}
private handleSubmitScript = () => {
this.getASTResponse(this.props.script)
}
private handleGenerateScript = (): void => {
this.getASTResponse(this.bodyToScript)
}
private handleChangeArg = ({
key,
value,
generate,
funcID,
declarationID = '',
bodyID,
}: InputArg): void => {
const body = this.state.body.map(b => {
if (b.id !== bodyID) {
return b
}
if (declarationID) {
const declarations = b.declarations.map(d => {
if (d.id !== declarationID) {
return d
}
const functions = this.editFuncArgs({
funcs: d.funcs,
funcID,
key,
value,
})
return {...d, funcs: functions}
})
return {...b, declarations}
}
const funcs = this.editFuncArgs({
funcs: b.funcs,
funcID,
key,
value,
})
return {...b, funcs}
})
this.setState({body}, () => {
if (generate) {
this.handleGenerateScript()
}
})
}
private editFuncArgs = ({funcs, funcID, key, value}): Func[] => {
return 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}
})
}
private get bodyToScript(): string {
return this.getBodyToScript(this.state.body)
}
private getBodyToScript(body: Body[]): string {
return body.reduce((acc, b) => {
if (b.declarations.length) {
const declaration = _.get(b, 'declarations.0', false)
if (!declaration) {
return acc
}
if (!declaration.funcs) {
return `${acc}${b.source}`
}
return `${acc}${declaration.name} = ${this.funcsToScript(
declaration.funcs
)}\n\n`
}
return `${acc}${this.funcsToScript(b.funcs)}\n\n`
}, '')
}
private funcsToScript(funcs): string {
return funcs
.map(func => `${func.name}(${this.argsToScript(func.args)})`)
.join('\n\t|> ')
}
private argsToScript(args): string {
const withValues = args.filter(arg => arg.value || arg.value === false)
return withValues
.map(({key, value, type}) => {
if (type === argTypes.STRING) {
return `${key}: "${value}"`
}
if (type === argTypes.ARRAY) {
return `${key}: ["${value}"]`
}
if (type === argTypes.OBJECT) {
const valueString = _.map(value, (v, k) => k + ':' + v).join(',')
return `${key}: {${valueString}}`
}
return `${key}: ${value}`
})
.join(', ')
}
private handleAppendFrom = (): void => {
const {script} = this.props
let newScript = script.trim()
const from = builder.NEW_FROM
if (!newScript) {
this.getASTResponse(from)
return
}
newScript = `${script.trim()}\n\n${from}\n\n`
this.getASTResponse(newScript)
}
private handleAppendJoin = (): void => {
const {script} = this.props
const newScript = `${script.trim()}\n\n${builder.NEW_JOIN}\n\n`
this.getASTResponse(newScript)
}
private handleChangeScript = (script: string): void => {
this.debouncedASTResponse(script)
this.props.updateScript(script)
}
private handleAddNode = (
name: string,
bodyID: string,
declarationID: string
): void => {
const script = this.state.body.reduce((acc, body) => {
const {id, source, funcs} = body
if (id === bodyID) {
const declaration = body.declarations.find(d => d.id === declarationID)
if (declaration) {
return `${acc}${declaration.name} = ${this.appendFunc(
declaration.funcs,
name
)}`
}
return `${acc}${this.appendFunc(funcs, name)}`
}
return `${acc}${this.formatSource(source)}`
}, '')
this.getASTResponse(script)
}
private handleDeleteBody = (bodyID: string): void => {
const newBody = this.state.body.filter(b => b.id !== bodyID)
const script = this.getBodyToScript(newBody)
this.getASTResponse(script)
}
private handleScriptUpToYield = (
bodyID: string,
declarationID: string,
funcNodeIndex: number,
isYieldable: boolean
): string => {
const {body: bodies} = this.state
const bodyIndex = bodies.findIndex(b => b.id === bodyID)
const bodiesBeforeYield = bodies
.slice(0, bodyIndex)
.map(b => this.removeYieldFuncFromBody(b))
const body = this.prepBodyForYield(
bodies[bodyIndex],
declarationID,
funcNodeIndex
)
const bodiesForScript = [...bodiesBeforeYield, body]
let script = this.getBodyToScript(bodiesForScript)
if (!isYieldable) {
const regex: RegExp = /\n{2}$/
script = script.replace(regex, '\n\t|> last()\n\t|> yield()$&')
return script
}
return script
}
private prepBodyForYield(
body: Body,
declarationID: string,
yieldNodeIndex: number
) {
const funcs = this.getFuncs(body, declarationID)
const funcsUpToYield = funcs.slice(0, yieldNodeIndex)
const yieldNode = funcs[yieldNodeIndex]
const funcsWithoutYields = funcsUpToYield.filter(f => f.name !== 'yield')
const funcsForBody = [...funcsWithoutYields, yieldNode]
if (declarationID) {
const declaration = body.declarations.find(d => d.id === declarationID)
const declarations = [{...declaration, funcs: funcsForBody}]
return {...body, declarations}
}
return {...body, funcs: funcsForBody}
}
private getFuncs(body: Body, declarationID: string): Func[] {
const declaration = body.declarations.find(d => d.id === declarationID)
if (declaration) {
return _.get(declaration, 'funcs', [])
}
return _.get(body, 'funcs', [])
}
private removeYieldFuncFromBody(body: Body): Body {
const declarationID = _.get(body, 'declarations.0.id')
const funcs = this.getFuncs(body, declarationID)
if (_.isEmpty(funcs)) {
return body
}
const funcsWithoutYields = funcs.filter(f => f.name !== 'yield')
if (declarationID) {
const declaration = _.get(body, 'declarations.0')
const declarations = [{...declaration, funcs: funcsWithoutYields}]
return {...body, declarations}
}
return {...body, funcs: funcsWithoutYields}
}
private handleToggleYield = (
bodyID: string,
declarationID: string,
funcNodeIndex: number
): void => {
const script = this.state.body.reduce((acc, body) => {
const {id, source, funcs} = body
if (id === bodyID) {
const declaration = body.declarations.find(d => d.id === declarationID)
if (declaration) {
return `${acc}${declaration.name} = ${this.addOrRemoveYieldFunc(
declaration.funcs,
funcNodeIndex
)}`
}
return `${acc}${this.addOrRemoveYieldFunc(funcs, funcNodeIndex)}`
}
return `${acc}${this.formatSource(source)}`
}, '')
this.getASTResponse(script)
}
private getNextYieldName = (): string => {
const yieldNamePrefix = 'results_'
const yieldNamePattern = `${yieldNamePrefix}(\\d+)`
const regex = new RegExp(yieldNamePattern)
const MIN = -1
const yieldsMaxResultNumber = this.state.body.reduce((scriptMax, body) => {
const {funcs: bodyFuncs, declarations} = body
let funcs = bodyFuncs
if (!_.isEmpty(declarations)) {
funcs = _.flatMap(declarations, d => _.get(d, 'funcs', []))
}
const yields = funcs.filter(f => f.name === 'yield')
const bodyMax = yields.reduce((max, y) => {
const yieldArg = _.get(y, 'args.0.value')
if (!yieldArg) {
return max
}
const yieldNumberString = _.get(yieldArg.match(regex), '1', `${MIN}`)
const yieldNumber = parseInt(yieldNumberString, 10)
return Math.max(yieldNumber, max)
}, scriptMax)
return Math.max(scriptMax, bodyMax)
}, MIN)
return `${yieldNamePrefix}${yieldsMaxResultNumber + 1}`
}
private addOrRemoveYieldFunc(funcs: Func[], funcNodeIndex: number): string {
if (funcNodeIndex < funcs.length - 1) {
const funcAfterNode = funcs[funcNodeIndex + 1]
if (funcAfterNode.name === 'yield') {
return this.removeYieldFunc(funcs, funcAfterNode)
}
}
return this.insertYieldFunc(funcs, funcNodeIndex)
}
private removeYieldFunc(funcs: Func[], funcAfterNode: Func): string {
const filteredFuncs = funcs.filter(f => f.id !== funcAfterNode.id)
return `${this.funcsToScript(filteredFuncs)}\n\n`
}
private appendFunc = (funcs: Func[], name: string): string => {
return `${this.funcsToScript(funcs)}\n\t|> ${name}()\n\n`
}
private insertYieldFunc(funcs: Func[], index: number): string {
const funcsBefore = funcs.slice(0, index + 1)
const funcsBeforeScript = this.funcsToScript(funcsBefore)
const funcsAfter = funcs.slice(index + 1)
const funcsAfterScript = this.funcsToScript(funcsAfter)
const funcSeparator = '\n\t|> '
if (funcsAfterScript) {
return `${funcsBeforeScript}${funcSeparator}yield(name: "${this.getNextYieldName()}")${funcSeparator}${funcsAfterScript}\n\n`
}
return `${funcsBeforeScript}${funcSeparator}yield(name: "${this.getNextYieldName()}")\n\n`
}
private handleDeleteFuncNode = (ids: DeleteFuncNodeArgs): void => {
const {funcID, declarationID = '', bodyID, yieldNodeID = ''} = ids
const script = this.state.body
.map((body, bodyIndex) => {
if (body.id !== bodyID) {
return this.formatSource(body.source)
}
const isLast = bodyIndex === this.state.body.length - 1
if (declarationID) {
const declaration = body.declarations.find(
d => d.id === declarationID
)
if (!declaration) {
return
}
const functions = declaration.funcs.filter(
f => f.id !== funcID && f.id !== yieldNodeID
)
const s = this.funcsToSource(functions)
return `${declaration.name} = ${this.formatLastSource(s, isLast)}`
}
const funcs = body.funcs.filter(
f => f.id !== funcID && f.id !== yieldNodeID
)
const source = this.funcsToSource(funcs)
return this.formatLastSource(source, isLast)
})
.join('')
this.getASTResponse(script)
}
private formatSource = (source: string): string => {
// currently a bug in the AST which does not add newlines to literal variable assignment bodies
if (!source.match(/\n\n/)) {
return `${source}\n\n`
}
return `${source}`
}
// formats the last line of a body string to include two new lines
private formatLastSource = (source: string, isLast: boolean): string => {
if (isLast) {
return `${source}`
}
// currently a bug in the AST which does not add newlines to literal variable assignment bodies
if (!source.match(/\n\n/)) {
return `${source}\n\n`
}
return `${source}\n\n`
}
// funcsToSource takes a list of funtion nodes and returns an flux script
private funcsToSource = (funcs): string => {
return funcs.reduce((acc, f, i) => {
if (i === 0) {
return `${f.source}`
}
return `${acc}\n\t${f.source}`
}, '')
}
private handleValidate = async () => {
const {links, notify, script} = this.props
try {
const ast = await getAST({url: links.ast, query: script})
const body = bodyNodes(ast, this.state.suggestions)
const status = {type: 'success', text: ''}
notify(validateSuccess())
this.setState({ast, body, status})
} catch (error) {
this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
}
}
private getASTResponse = async (script: string, update: boolean = true) => {
const {links} = this.props
if (!script) {
this.props.updateScript(script)
return this.setState({ast: emptyAST, body: []})
}
try {
const ast = await getAST({url: links.ast, query: script})
if (update) {
this.props.updateScript(script)
}
const body = bodyNodes(ast, this.state.suggestions)
const status = {type: 'success', text: ''}
this.setState({ast, body, status})
} catch (error) {
this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
}
}
private getTimeSeries = async () => {
const {script, links, notify, source} = this.props
if (!script) {
return
}
try {
await getAST({url: links.ast, query: script})
} catch (error) {
this.setState({status: this.parseError(error)})
return console.error('Could not parse AST', error)
}
try {
const {tables, didTruncate} = await getTimeSeries(
source.links.query,
script
)
this.setState({data: tables})
if (didTruncate) {
notify(fluxResponseTruncatedError())
}
} catch (error) {
this.setState({data: []})
notify(fluxTimeSeriesError(error))
console.error('Could not get timeSeries', error)
}
this.getASTResponse(script)
}
private parseError = (error): Status => {
const s = error.data.slice(0, -5) // There is a 'null\n' at the end of these responses
const data = JSON.parse(s)
return {type: 'error', text: `${data.message}`}
}
}
const mdtp = {
updateScript: updateScriptAction,
notify: notifyAction,
}
const mstp = ({links, script}) => {
return {
links: links.query,
script,
}
}
export default connect(mstp, mdtp)(FluxPage)

View File

@ -1,148 +0,0 @@
import {bodyNodes} from 'src/flux/helpers'
import suggestions from 'src/flux/helpers/stubs/suggestions'
import Variables from 'src/flux/ast/stubs/variables'
import {Expression, StringLiteral} from 'src/flux/ast/stubs/variable'
import From from 'src/flux/ast/stubs/from'
const id = expect.any(String)
describe('Flux.helpers', () => {
describe('bodyNodes', () => {
describe('bodyNodes for Expressions assigned to a variable', () => {
it('can parse an Expression assigned to a Variable', () => {
const actual = bodyNodes(Expression, suggestions)
const expected = [
{
declarations: [
{
funcs: [
{
args: [{key: 'db', type: 'string', value: 'telegraf'}],
id,
name: 'from',
source: 'from(db: "telegraf")',
},
],
id,
name: 'tele',
source: 'tele = from(db: "telegraf")',
type: 'CallExpression',
},
],
id,
source: 'tele = from(db: "telegraf")',
type: 'VariableDeclaration',
},
]
expect(actual).toEqual(expected)
})
})
describe('bodyNodes for a Literal assigned to a Variable', () => {
it('can parse an Expression assigned to a Variable', () => {
const actual = bodyNodes(StringLiteral, suggestions)
const expected = [
{
id,
source: 'bux = "im a var"',
type: 'VariableDeclaration',
declarations: [
{
id,
name: 'bux',
type: 'StringLiteral',
value: 'im a var',
},
],
},
]
expect(actual).toEqual(expected)
})
})
describe('bodyNodes for an Expression', () => {
it('can parse an Expression into bodyNodes', () => {
const actual = bodyNodes(From, suggestions)
const expected = [
{
declarations: [],
funcs: [
{
args: [{key: 'db', type: 'string', value: 'telegraf'}],
id,
name: 'from',
source: 'from(db: "telegraf")',
},
],
id,
source: 'from(db: "telegraf")',
type: 'CallExpression',
},
]
expect(actual).toEqual(expected)
})
})
})
describe('multiple bodyNodes', () => {
it('can parse variables and expressions together', () => {
const actual = bodyNodes(Variables, suggestions)
const expected = [
{
declarations: [
{
id,
name: 'bux',
type: 'StringLiteral',
value: 'ASDFASDFASDF',
},
],
id,
source: 'bux = "ASDFASDFASDF"',
type: 'VariableDeclaration',
},
{
declarations: [
{
funcs: [
{
args: [{key: 'db', type: 'string', value: 'foo'}],
id,
name: 'from',
source: 'from(db: "foo")',
},
],
id,
name: 'foo',
source: 'foo = from(db: "foo")',
type: 'CallExpression',
},
],
id,
source: 'foo = from(db: "foo")',
type: 'VariableDeclaration',
},
{
declarations: [],
funcs: [
{
args: [{key: 'db', type: 'string', value: 'bux'}],
id,
name: 'from',
source: 'from(db: bux)',
},
],
id,
source: 'from(db: bux)',
type: 'CallExpression',
},
]
expect(actual).toEqual(expected)
})
})
})

View File

@ -1,87 +0,0 @@
import uuid from 'uuid'
import _ from 'lodash'
import Walker from 'src/flux/ast/walker'
import {FlatBody, Func, Suggestion} from 'src/types/flux'
interface Body extends FlatBody {
id: string
}
export const bodyNodes = (ast, suggestions: Suggestion[]): Body[] => {
if (!ast) {
return []
}
const walker = new Walker(ast)
const body = walker.body.map(b => {
const {type} = b
const id = uuid.v4()
if (type.includes('Variable')) {
const declarations = b.declarations.map(d => {
if (!d.funcs) {
return {...d, id: uuid.v4()}
}
return {
...d,
id: uuid.v4(),
funcs: functions(d.funcs, suggestions),
}
})
return {...b, type, id, declarations}
}
const {funcs, source} = b
return {
id,
funcs: functions(funcs, suggestions),
declarations: [],
type,
source,
}
})
return body
}
const functions = (funcs: Func[], suggestions: Suggestion[]): Func[] => {
const funcList = funcs.map(func => {
const suggestion = suggestions.find(f => f.name === func.name)
if (!suggestion) {
return {
type: func.type,
id: uuid.v4(),
source: func.source,
name: func.name,
args: func.args,
}
}
const {params, name} = suggestion
const args = Object.entries(params).map(([key, type]) => {
const argWithKey = func.args.find(arg => arg.key === key)
const value = _.get(argWithKey, 'value', '')
return {
key,
value,
type,
}
})
return {
type: func.type,
id: uuid.v4(),
source: func.source,
name,
args,
}
})
return funcList
}

View File

@ -1,93 +0,0 @@
export default [
{
name: '_highestOrLowest',
params: {
_sortLimit: 'invalid',
by: 'invalid',
cols: 'array',
n: 'invalid',
reducer: 'function',
},
},
{
name: '_sortLimit',
params: {cols: 'array', desc: 'invalid', n: 'invalid'},
},
{name: 'bottom', params: {cols: 'array', n: 'invalid'}},
{name: 'count', params: {}},
{
name: 'cov',
params: {on: 'invalid', pearsonr: 'bool', x: 'invalid', y: 'invalid'},
},
{name: 'covariance', params: {pearsonr: 'bool'}},
{name: 'derivative', params: {nonNegative: 'bool', unit: 'duration'}},
{name: 'difference', params: {nonNegative: 'bool'}},
{name: 'distinct', params: {column: 'string'}},
{name: 'filter', params: {fn: 'function'}},
{name: 'first', params: {column: 'string', useRowTime: 'bool'}},
{name: 'from', params: {db: 'string'}},
{name: 'group', params: {by: 'array', except: 'array', keep: 'array'}},
{
name: 'highestAverage',
params: {by: 'invalid', cols: 'array', n: 'invalid'},
},
{
name: 'highestCurrent',
params: {by: 'invalid', cols: 'array', n: 'invalid'},
},
{name: 'highestMax', params: {by: 'invalid', cols: 'array', n: 'invalid'}},
{name: 'integral', params: {unit: 'duration'}},
{name: 'join', params: {}},
{name: 'last', params: {column: 'string', useRowTime: 'bool'}},
{name: 'limit', params: {}},
{
name: 'lowestAverage',
params: {by: 'invalid', cols: 'array', n: 'invalid'},
},
{
name: 'lowestCurrent',
params: {by: 'invalid', cols: 'array', n: 'invalid'},
},
{name: 'lowestMin', params: {by: 'invalid', cols: 'array', n: 'invalid'}},
{name: 'map', params: {fn: 'function'}},
{name: 'max', params: {column: 'string', useRowTime: 'bool'}},
{name: 'mean', params: {}},
{name: 'median', params: {compression: 'float', exact: 'bool'}},
{name: 'min', params: {column: 'string', useRowTime: 'bool'}},
{name: 'pearsonr', params: {on: 'invalid', x: 'invalid', y: 'invalid'}},
{name: 'percentile', params: {p: 'float'}},
{name: 'range', params: {start: 'time', stop: 'time'}},
{name: 'sample', params: {column: 'string', useRowTime: 'bool'}},
{name: 'set', params: {key: 'string', value: 'string'}},
{name: 'shift', params: {shift: 'duration'}},
{name: 'skew', params: {}},
{name: 'sort', params: {cols: 'array'}},
{name: 'spread', params: {}},
{name: 'stateCount', params: {fn: 'invalid', label: 'string'}},
{
name: 'stateDuration',
params: {fn: 'invalid', label: 'string', unit: 'duration'},
},
{
name: 'stateTracking',
params: {
countLabel: 'string',
durationLabel: 'string',
durationUnit: 'duration',
fn: 'function',
},
},
{name: 'stddev', params: {}},
{name: 'sum', params: {}},
{name: 'top', params: {cols: 'array', n: 'invalid'}},
{
name: 'window',
params: {
every: 'duration',
period: 'duration',
round: 'duration',
start: 'time',
},
},
{name: 'yield', params: {name: 'string'}},
]

View File

@ -1,3 +0,0 @@
import FluxPage from 'src/flux/containers/FluxPage'
export {FluxPage}

View File

@ -1,17 +0,0 @@
import {Action, ActionTypes} from 'src/flux/actions'
import {editor} from 'src/flux/constants'
const scriptReducer = (
state: string = editor.DEFAULT_SCRIPT,
action: Action
): string => {
switch (action.type) {
case ActionTypes.UpdateScript: {
return action.payload.script
}
}
return state
}
export default scriptReducer

View File

@ -22,7 +22,6 @@ import Signin from 'src/Signin'
import {DashboardsPage, DashboardPage} from 'src/dashboards'
import DataExplorerPage from 'src/dataExplorer/components/DataExplorerPage'
import {SourcePage, ManageSources} from 'src/sources'
import {FluxPage} from 'src/flux'
import {UserPage} from 'src/user'
import {LogsPage} from 'src/logs'
import NotFound from 'src/shared/components/NotFound'
@ -119,7 +118,6 @@ class Root extends PureComponent<{}, State> {
path="manage-sources/:id/edit"
component={SourcePage}
/>
<Route path="delorean" component={FluxPage} />
<Route path="user/:tab" component={UserPage} />
<Route path="logs" component={LogsPage} />
</Route>

View File

@ -66,11 +66,11 @@ class SideNav extends PureComponent<Props> {
},
{
type: NavItemType.Icon,
title: 'Flux Builder',
link: `/delorean/${this.sourceParam}`,
title: 'Data Explorer',
link: `/data-explorer/${this.sourceParam}`,
icon: IconFont.Capacitor,
location: location.pathname,
highlightWhen: ['delorean'],
highlightWhen: ['data-explorer'],
},
{
type: NavItemType.Icon,

View File

@ -1,24 +0,0 @@
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

@ -41,9 +41,7 @@ const timeMachineReducer = (
action: Action
): TimeMachinesState => {
if (action.type === 'SET_ACTIVE_TIME_MACHINE_ID') {
const {activeTimeMachineID} = action.payload
return {...state, activeTimeMachineID}
return {...state, activeTimeMachineID: action.payload.activeTimeMachineID}
}
// All other actions act upon whichever single `TimeMachineState` is

View File

@ -7,7 +7,6 @@ import {resizeLayout} from 'src/shared/middleware/resizeLayout'
import {queryStringConfig} from 'src/shared/middleware/queryStringConfig'
import sharedReducers from 'src/shared/reducers'
import persistStateEnhancer from './persistStateEnhancer'
import scriptReducer from 'src/flux/reducers/script'
import sourceReducer from 'src/sources/reducers/sources'
// v2 reducers
@ -25,7 +24,6 @@ const rootReducer = combineReducers({
dashboards: dashboardsReducer,
timeMachines: timeMachinesReducer,
routing: routerReducer,
script: scriptReducer,
sources: sourceReducer,
views: viewsReducer,
logs: logsReducer,

View File

@ -44,13 +44,7 @@
@import 'src/dashboards/components/rename_dashboard/RenameDashboard';
@import 'src/dashboards/components/dashboard_empty/DashboardEmpty';
@import 'src/dashboards/components/VEO';
@import 'src/flux/components/time_machine/TimeMachine';
@import 'src/flux/components/time_machine/flux-overlay';
@import 'src/flux/components/time_machine/flux-editor';
@import 'src/flux/components/time_machine/flux-builder';
@import 'src/flux/components/time_machine/flux-explorer';
@import 'src/flux/components/time_machine/visualization';
@import 'src/flux/components/time_machine/add-func-button';
@import 'src/dataExplorer/components/DataExplorer';
@import 'src/shared/components/views/Markdown';
@import 'src/onboarding/OnboardingWizard.scss';

View File

@ -16,6 +16,7 @@
"interface-name": [true, "never-prefix"],
"no-console": [true, "log", "warn"],
"no-empty": false,
"no-empty-interface": false,
"prefer-for-of": false,
"prettier": [
true,