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
parent
8b17102a7d
commit
bbd7153cca
|
@ -0,0 +1,3 @@
|
|||
.data-explorer {
|
||||
padding: 32px;
|
||||
}
|
|
@ -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
|
||||
>
|
||||
|
|
|
@ -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},
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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")'
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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
|
|
@ -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})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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}`
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
.flux-overlay {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
|
|
|
@ -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'
|
|
@ -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: [],
|
||||
},
|
||||
}
|
|
@ -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"])`
|
|
@ -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>'
|
|
@ -1 +0,0 @@
|
|||
export const TAG_VALUES_LIMIT = 10
|
|
@ -1,3 +0,0 @@
|
|||
export const FROM = 'from'
|
||||
export const FILTER = 'filter'
|
||||
export const JOIN = 'join'
|
|
@ -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
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
export const TABLE_ROW_HEADER_HEIGHT = 40
|
||||
export const TABLE_ROW_HEIGHT = 30
|
||||
export const TIME_COLUMN_WIDTH = 170
|
|
@ -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)
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -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
|
||||
}
|
|
@ -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'}},
|
||||
]
|
|
@ -1,3 +0,0 @@
|
|||
import FluxPage from 'src/flux/containers/FluxPage'
|
||||
|
||||
export {FluxPage}
|
|
@ -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
|
|
@ -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>
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue