Merge branch 'master' into presentational-page-components

pull/3662/head
Alex P 2018-06-19 10:32:27 -07:00
commit 9798f0ef0c
25 changed files with 451 additions and 182 deletions

138
.circleci/config.yml Normal file
View File

@ -0,0 +1,138 @@
workflows:
version: 2
main:
jobs:
- build
- deploy-nightly:
requires:
- build
filters:
branches:
only: master
- deploy-pre-release:
requires:
- build
filters:
branches:
ignore: /.*/
tags:
only: /^[0-9]+(\.[0-9]+)*(\S*)([a|rc|beta]([0-9]+))+$/
- deploy-release:
requires:
- build
filters:
branches:
ignore: /.*/
tags:
only: /^[0-9]+(\.[0-9]+)*$/
version: 2
jobs:
build:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- checkout
- run: |
ls -lah
pwd
- run: ./etc/scripts/docker/pull.sh
- run:
name: "Run Tests"
command: >
./etc/scripts/docker/run.sh
--debug
--test
--no-build
- persist_to_workspace:
root: /home/circleci
paths:
- project
deploy-nightly:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- attach_workspace:
at: /home/circleci
- run: |
./etc/scripts/docker/run.sh \
--debug \
--clean \
--package \
--platform all \
--arch all \
--upload \
--nightly \
--bucket=dl.influxdata.com/chronograf/releases
cp build/linux/static_amd64/chronograf .
cp build/linux/static_amd64/chronoctl .
docker build -t chronograf .
docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
docker tag chronograf quay.io/influxdb/chronograf:nightly
docker push quay.io/influxdb/chronograf:nightly
- store_artifacts:
path: ./build/
deploy-pre-release:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- attach_workspace:
at: /home/circleci
- run: |
./etc/scripts/docker/run.sh \
--clean \
--debug \
--release \
--package \
--platform all \
--arch all \
--upload-overwrite \
--upload \
--bucket dl.influxdata.com/chronograf/releases
cp build/linux/static_amd64/chronograf .
cp build/linux/static_amd64/chronoctl .
docker build -t chronograf .
docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
- store_artifacts:
path: ./build/
deploy-release:
environment:
DOCKER_TAG: chronograf-20180327
machine: true
steps:
- attach_workspace:
at: /home/circleci
- run: |
./etc/scripts/docker/run.sh \
--clean \
--debug \
--release \
--package \
--platform all \
--arch all \
--upload-overwrite \
--upload \
--bucket dl.influxdata.com/chronograf/releases
cp build/linux/static_amd64/chronograf .
cp build/linux/static_amd64/chronoctl .
docker build -t chronograf .
docker login -u "$QUAY_USER" -p $QUAY_PASS quay.io
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
docker tag chronograf quay.io/influxdb/chronograf:latest
docker push quay.io/influxdb/chronograf:latest
- store_artifacts:
path: ./build/

View File

@ -10,6 +10,7 @@
1. [#3474](https://github.com/influxdata/chronograf/pull/3474): Sort task table on Manage Alert page alphabetically 1. [#3474](https://github.com/influxdata/chronograf/pull/3474): Sort task table on Manage Alert page alphabetically
1. [#3590](https://github.com/influxdata/chronograf/pull/3590): Redesign icons in side navigation 1. [#3590](https://github.com/influxdata/chronograf/pull/3590): Redesign icons in side navigation
1. [#3696](https://github.com/influxdata/chronograf/pull/3696): Add ability to delete entire queries in Flux Editor
1. [#3671](https://github.com/influxdata/chronograf/pull/3671): Remove Snip functionality in hover legend 1. [#3671](https://github.com/influxdata/chronograf/pull/3671): Remove Snip functionality in hover legend
1. [#3659](https://github.com/influxdata/chronograf/pull/3659): Upgrade Data Explorer query text field with syntax highlighting and partial multi-line support 1. [#3659](https://github.com/influxdata/chronograf/pull/3659): Upgrade Data Explorer query text field with syntax highlighting and partial multi-line support
1. [#3663](https://github.com/influxdata/chronograf/pull/3663): Truncate message preview in Alert Rules table 1. [#3663](https://github.com/influxdata/chronograf/pull/3663): Truncate message preview in Alert Rules table

View File

@ -1,91 +0,0 @@
---
machine:
services:
- docker
environment:
DOCKER_TAG: chronograf-20180327
dependencies:
override:
- ./etc/scripts/docker/pull.sh
test:
override:
- >
./etc/scripts/docker/run.sh
--debug
--test
--no-build
deployment:
master:
branch: master
commands:
- >
./etc/scripts/docker/run.sh
--debug
--clean
--package
--platform all
--arch all
--upload
--nightly
--bucket=dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:nightly
- docker push quay.io/influxdb/chronograf:nightly
- mv ./build/* $CIRCLE_ARTIFACTS
pre-release:
tag: /^[0-9]+(\.[0-9]+)*(\S*)([a|rc|beta]([0-9]+))+$/
commands:
- >
./etc/scripts/docker/run.sh
--clean
--debug
--release
--package
--platform all
--arch all
--upload-overwrite
--upload
--bucket dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
- docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
- mv ./build/* $CIRCLE_ARTIFACTS
release:
tag: /^[0-9]+(\.[0-9]+)*$/
commands:
- >
./etc/scripts/docker/run.sh
--clean
--debug
--release
--package
--platform all
--arch all
--upload-overwrite
--upload
--bucket dl.influxdata.com/chronograf/releases
- sudo chown -R ubuntu:ubuntu /home/ubuntu
- cp build/linux/static_amd64/chronograf .
- cp build/linux/static_amd64/chronoctl .
- docker build -t chronograf .
- docker login -e $QUAY_EMAIL -u "$QUAY_USER" -p $QUAY_PASS quay.io
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker push quay.io/influxdb/chronograf:${CIRCLE_SHA1:0:7}
- docker tag chronograf quay.io/influxdb/chronograf:${CIRCLE_TAG}
- docker push quay.io/influxdb/chronograf:${CIRCLE_TAG}
- docker tag chronograf quay.io/influxdb/chronograf:latest
- docker push quay.io/influxdb/chronograf:latest
- mv ./build/* $CIRCLE_ARTIFACTS

View File

@ -12,7 +12,7 @@ import {
applyMasks, applyMasks,
insertTempVar, insertTempVar,
unMask, unMask,
} from 'src/dashboards/constants' } from 'src/tempVars/constants'
@ErrorHandling @ErrorHandling
class QueryTextArea extends Component { class QueryTextArea extends Component {

View File

@ -32,7 +32,7 @@ export const generateURLQueryParamsFromTempVars = (
const selected = values.find(value => value.selected === true) const selected = values.find(value => value.selected === true)
const strippedTempVar = stripTempVar(tempVar) const strippedTempVar = stripTempVar(tempVar)
urlQueryParams[strippedTempVar] = selected.value urlQueryParams[strippedTempVar] = _.get(selected, 'value', '')
}) })
return urlQueryParams return urlQueryParams

View File

@ -3,12 +3,13 @@ import _ from 'lodash'
import FancyScrollbar from 'src/shared/components/FancyScrollbar' import FancyScrollbar from 'src/shared/components/FancyScrollbar'
import ExpressionNode from 'src/flux/components/ExpressionNode' import ExpressionNode from 'src/flux/components/ExpressionNode'
import VariableName from 'src/flux/components/VariableName' import VariableNode from 'src/flux/components/VariableNode'
import FuncSelector from 'src/flux/components/FuncSelector' import FuncSelector from 'src/flux/components/FuncSelector'
import BodyDelete from 'src/flux/components/BodyDelete'
import {funcNames} from 'src/flux/constants' import {funcNames} from 'src/flux/constants'
import {Service} from 'src/types' import {Service} from 'src/types'
import {FlatBody, Suggestion} from 'src/types/flux' import {Body, Suggestion} from 'src/types/flux'
interface Props { interface Props {
service: Service service: Service
@ -16,21 +17,25 @@ interface Props {
suggestions: Suggestion[] suggestions: Suggestion[]
onAppendFrom: () => void onAppendFrom: () => void
onAppendJoin: () => void onAppendJoin: () => void
} onDeleteBody: (bodyID: string) => void
interface Body extends FlatBody {
id: string
} }
class BodyBuilder extends PureComponent<Props> { class BodyBuilder extends PureComponent<Props> {
public render() { public render() {
const bodybuilder = this.props.body.map((b, i) => { const {body, onDeleteBody} = this.props
const bodybuilder = body.map((b, i) => {
if (b.declarations.length) { if (b.declarations.length) {
return b.declarations.map(d => { return b.declarations.map(d => {
if (d.funcs) { if (d.funcs) {
return ( return (
<div className="declaration" key={i}> <div className="declaration" key={i}>
<VariableName name={d.name} assignedToQuery={true} /> <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 <ExpressionNode
bodyID={b.id} bodyID={b.id}
declarationID={d.id} declarationID={d.id}
@ -38,6 +43,7 @@ class BodyBuilder extends PureComponent<Props> {
funcs={d.funcs} funcs={d.funcs}
declarationsFromBody={this.declarationsFromBody} declarationsFromBody={this.declarationsFromBody}
isLastBody={this.isLastBody(i)} isLastBody={this.isLastBody(i)}
onDeleteBody={onDeleteBody}
/> />
</div> </div>
) )
@ -45,7 +51,16 @@ class BodyBuilder extends PureComponent<Props> {
return ( return (
<div className="declaration" key={i}> <div className="declaration" key={i}>
<VariableName name={b.source} assignedToQuery={false} /> <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> </div>
) )
}) })
@ -59,6 +74,7 @@ class BodyBuilder extends PureComponent<Props> {
funcNames={this.funcNames} funcNames={this.funcNames}
declarationsFromBody={this.declarationsFromBody} declarationsFromBody={this.declarationsFromBody}
isLastBody={this.isLastBody(i)} isLastBody={this.isLastBody(i)}
onDeleteBody={onDeleteBody}
/> />
</div> </div>
) )

View File

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

View File

@ -15,6 +15,7 @@ interface Props {
declarationID?: string declarationID?: string
declarationsFromBody: string[] declarationsFromBody: string[]
isLastBody: boolean isLastBody: boolean
onDeleteBody: (bodyID: string) => void
} }
interface State { interface State {
@ -42,6 +43,7 @@ class ExpressionNode extends PureComponent<Props, State> {
funcNames, funcNames,
funcs, funcs,
declarationsFromBody, declarationsFromBody,
onDeleteBody,
} = this.props } = this.props
const {nonYieldableIndexesToggled} = this.state const {nonYieldableIndexesToggled} = this.state
@ -106,6 +108,7 @@ class ExpressionNode extends PureComponent<Props, State> {
onGenerateScript={onGenerateScript} onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody} declarationsFromBody={declarationsFromBody}
onToggleYieldWithLast={this.handleToggleYieldWithLast} onToggleYieldWithLast={this.handleToggleYieldWithLast}
onDeleteBody={onDeleteBody}
/> />
) )
@ -152,6 +155,7 @@ class ExpressionNode extends PureComponent<Props, State> {
onGenerateScript={onGenerateScript} onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody} declarationsFromBody={declarationsFromBody}
onToggleYieldWithLast={this.handleToggleYieldWithLast} onToggleYieldWithLast={this.handleToggleYieldWithLast}
onDeleteBody={onDeleteBody}
/> />
<YieldFuncNode <YieldFuncNode
index={i} index={i}

View File

@ -5,9 +5,11 @@ import {tagKeys as fetchTagKeys} from 'src/shared/apis/flux/metaQueries'
import parseValuesColumn from 'src/shared/parsing/flux/values' import parseValuesColumn from 'src/shared/parsing/flux/values'
import FilterTagList from 'src/flux/components/FilterTagList' import FilterTagList from 'src/flux/components/FilterTagList'
import Walker from 'src/flux/ast/walker' import Walker from 'src/flux/ast/walker'
import {makeCancelable} from 'src/utils/promises'
import {Service} from 'src/types' import {Service} from 'src/types'
import {Links, OnChangeArg, Func, FilterNode} from 'src/types/flux' import {Links, OnChangeArg, Func, FilterNode} from 'src/types/flux'
import {WrappedCancelablePromise} from 'src/types/promises'
interface Props { interface Props {
links: Links links: Links
@ -28,6 +30,8 @@ interface State {
} }
class FilterArgs extends PureComponent<Props, State> { class FilterArgs extends PureComponent<Props, State> {
private fetchTagKeysResponse?: WrappedCancelablePromise<string>
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@ -52,15 +56,24 @@ class FilterArgs extends PureComponent<Props, State> {
} }
public async componentDidMount() { public async componentDidMount() {
const {db, service} = this.props
try { try {
this.convertStringToNodes() this.convertStringToNodes()
const response = await fetchTagKeys(service, db, []) const response = await this.getTagKeys()
const tagKeys = parseValuesColumn(response) const tagKeys = parseValuesColumn(response)
this.setState({tagKeys})
this.setState({
tagKeys,
})
} catch (error) { } catch (error) {
console.error(error) if (!error.isCanceled) {
console.error(error)
}
}
}
public componentWillUnmount() {
if (this.fetchTagKeysResponse) {
this.fetchTagKeysResponse.cancel()
} }
} }
@ -91,6 +104,14 @@ class FilterArgs extends PureComponent<Props, State> {
/> />
) )
} }
private getTagKeys(): Promise<string> {
const {db, service} = this.props
this.fetchTagKeysResponse = makeCancelable(fetchTagKeys(service, db, []))
return this.fetchTagKeysResponse.promise
}
} }
const mapStateToProps = ({links}) => { const mapStateToProps = ({links}) => {

View File

@ -2,6 +2,7 @@ import React, {PureComponent, MouseEvent} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
import _ from 'lodash' import _ from 'lodash'
import BodyDelete from 'src/flux/components/BodyDelete'
import FuncArgs from 'src/flux/components/FuncArgs' import FuncArgs from 'src/flux/components/FuncArgs'
import FuncArgsPreview from 'src/flux/components/FuncArgsPreview' import FuncArgsPreview from 'src/flux/components/FuncArgsPreview'
import { import {
@ -28,6 +29,7 @@ interface Props {
declarationsFromBody: string[] declarationsFromBody: string[]
isYielding: boolean isYielding: boolean
isYieldable: boolean isYieldable: boolean
onDeleteBody: (bodyID: string) => void
} }
interface State { interface State {
@ -53,14 +55,16 @@ export default class FuncNode extends PureComponent<Props, State> {
return ( return (
<> <>
<div <div className="func-node--wrapper">
className={this.nodeClassName} <div
onClick={this.handleToggleEdit} className={this.nodeClassName}
title="Edit function arguments" onClick={this.handleToggleEdit}
> title="Edit function arguments"
<div className="func-node--connector" /> >
<div className="func-node--name">{func.name}</div> <div className="func-node--connector" />
<FuncArgsPreview func={func} /> <div className="func-node--name">{func.name}</div>
<FuncArgsPreview func={func} />
</div>
{this.funcMenu} {this.funcMenu}
</div> </div>
{this.funcArgs} {this.funcArgs}
@ -103,13 +107,7 @@ export default class FuncNode extends PureComponent<Props, State> {
return ( return (
<div className="func-node--menu"> <div className="func-node--menu">
{this.yieldToggleButton} {this.yieldToggleButton}
<button {this.deleteButton}
className="btn btn-sm btn-square btn-danger"
onClick={this.handleDelete}
title="Delete this Function"
>
<span className="icon trash" />
</button>
</div> </div>
) )
} }
@ -140,6 +138,24 @@ export default class FuncNode extends PureComponent<Props, State> {
) )
} }
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 { private get nodeClassName(): string {
const {isYielding} = this.props const {isYielding} = this.props
const {editing} = this.state const {editing} = this.state
@ -147,14 +163,14 @@ export default class FuncNode extends PureComponent<Props, State> {
return classnames('func-node', {active: isYielding || editing}) return classnames('func-node', {active: isYielding || editing})
} }
private handleDelete = (e: MouseEvent<HTMLElement>): void => { private handleDelete = (): void => {
e.stopPropagation()
const {func, bodyID, declarationID} = this.props const {func, bodyID, declarationID} = this.props
this.props.onDelete({funcID: func.id, bodyID, declarationID}) this.props.onDelete({funcID: func.id, bodyID, declarationID})
} }
private handleToggleEdit = (): void => { private handleToggleEdit = (e: MouseEvent<HTMLElement>): void => {
e.stopPropagation()
this.setState({editing: !this.state.editing}) this.setState({editing: !this.state.editing})
} }

View File

@ -7,6 +7,7 @@ import {
Suggestion, Suggestion,
OnChangeScript, OnChangeScript,
OnSubmitScript, OnSubmitScript,
OnDeleteBody,
FlatBody, FlatBody,
ScriptStatus, ScriptStatus,
} from 'src/types/flux' } from 'src/types/flux'
@ -22,6 +23,7 @@ interface Props {
status: ScriptStatus status: ScriptStatus
suggestions: Suggestion[] suggestions: Suggestion[]
onChangeScript: OnChangeScript onChangeScript: OnChangeScript
onDeleteBody: OnDeleteBody
onSubmitScript: OnSubmitScript onSubmitScript: OnSubmitScript
onAppendFrom: () => void onAppendFrom: () => void
onAppendJoin: () => void onAppendJoin: () => void
@ -63,7 +65,14 @@ class TimeMachine extends PureComponent<Props> {
} }
private get builder() { private get builder() {
const {body, service, suggestions, onAppendFrom, onAppendJoin} = this.props const {
body,
service,
suggestions,
onAppendFrom,
onDeleteBody,
onAppendJoin,
} = this.props
return { return {
name: 'Build', name: 'Build',
@ -75,6 +84,7 @@ class TimeMachine extends PureComponent<Props> {
body={body} body={body}
service={service} service={service}
suggestions={suggestions} suggestions={suggestions}
onDeleteBody={onDeleteBody}
onAppendFrom={onAppendFrom} onAppendFrom={onAppendFrom}
onAppendJoin={onAppendJoin} onAppendJoin={onAppendJoin}
/> />

View File

@ -1,3 +1,19 @@
export const emptyAST = {
type: 'Program',
location: {
start: {
line: 1,
column: 1,
},
end: {
line: 1,
column: 1,
},
source: '',
},
body: [],
}
export const ast = { export const ast = {
type: 'File', type: 'File',
start: 0, start: 0,

View File

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

View File

@ -15,7 +15,7 @@ import {UpdateScript} from 'src/flux/actions'
import {bodyNodes} from 'src/flux/helpers' import {bodyNodes} from 'src/flux/helpers'
import {getSuggestions, getAST, getTimeSeries} from 'src/flux/apis' import {getSuggestions, getAST, getTimeSeries} from 'src/flux/apis'
import {builder, argTypes} from 'src/flux/constants' import {builder, argTypes, emptyAST} from 'src/flux/constants'
import {Source, Service, Notification, FluxTable} from 'src/types' import {Source, Service, Notification, FluxTable} from 'src/types'
import { import {
@ -114,6 +114,7 @@ export class FluxPage extends PureComponent<Props, State> {
onAppendJoin={this.handleAppendJoin} onAppendJoin={this.handleAppendJoin}
onChangeScript={this.handleChangeScript} onChangeScript={this.handleChangeScript}
onSubmitScript={this.handleSubmitScript} onSubmitScript={this.handleSubmitScript}
onDeleteBody={this.handleDeleteBody}
/> />
</div> </div>
</KeyboardShortcuts> </KeyboardShortcuts>
@ -331,6 +332,13 @@ export class FluxPage extends PureComponent<Props, State> {
this.getASTResponse(script) 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 = ( private handleScriptUpToYield = (
bodyID: string, bodyID: string,
declarationID: string, declarationID: string,
@ -601,7 +609,8 @@ export class FluxPage extends PureComponent<Props, State> {
const {links} = this.props const {links} = this.props
if (!script) { if (!script) {
return this.props.updateScript(script)
return this.setState({ast: emptyAST, body: []})
} }
try { try {

View File

@ -45,6 +45,10 @@ class AnnotationInput extends Component<Props, State> {
</div> </div>
) )
} }
public handleClickOutside = () => {
this.props.onConfirmUpdate()
this.setState({isEditing: false})
}
private handleInputClick = () => { private handleInputClick = () => {
this.setState({isEditing: true}) this.setState({isEditing: true})

View File

@ -1,7 +1,10 @@
import React, {PureComponent} from 'react' import React, {PureComponent} from 'react'
import classnames from 'classnames'
import {ClickOutside} from 'src/shared/components/ClickOutside' import {ClickOutside} from 'src/shared/components/ClickOutside'
import {ErrorHandling} from 'src/shared/decorators/errors' import {ErrorHandling} from 'src/shared/decorators/errors'
type Position = 'top' | 'bottom' | 'left' | 'right'
interface Props { interface Props {
text?: string text?: string
confirmText?: string confirmText?: string
@ -12,6 +15,7 @@ interface Props {
icon?: string icon?: string
disabled?: boolean disabled?: boolean
customClass?: string customClass?: string
position?: Position
} }
interface State { interface State {
@ -47,10 +51,11 @@ class ConfirmButton extends PureComponent<Props, State> {
className={this.className} className={this.className}
onClick={this.handleButtonClick} onClick={this.handleButtonClick}
ref={r => (this.buttonDiv = r)} ref={r => (this.buttonDiv = r)}
title={confirmText}
> >
{icon && <span className={`icon ${icon}`} />} {icon && <span className={`icon ${icon}`} />}
{text && text} {text && text}
<div className={`confirm-button--tooltip ${this.calculatedPosition}`}> <div className={this.tooltipClassName}>
<div <div
className="confirm-button--confirmation" className="confirm-button--confirmation"
onClick={this.handleConfirmClick} onClick={this.handleConfirmClick}
@ -64,18 +69,6 @@ class ConfirmButton extends PureComponent<Props, State> {
) )
} }
private get className() {
const {type, size, square, disabled, customClass} = this.props
const {expanded} = this.state
const customClassString = customClass ? ` ${customClass}` : ''
const squareString = square ? ' btn-square' : ''
const expandedString = expanded ? ' active' : ''
const disabledString = disabled ? ' disabled' : ''
return `confirm-button btn ${type} ${size}${customClassString}${squareString}${expandedString}${disabledString}`
}
private handleButtonClick = () => { private handleButtonClick = () => {
if (this.props.disabled) { if (this.props.disabled) {
return return
@ -92,9 +85,27 @@ class ConfirmButton extends PureComponent<Props, State> {
this.setState({expanded: false}) this.setState({expanded: false})
} }
private get calculatedPosition() { private get className(): string {
const {type, size, square, disabled, customClass} = this.props
const {expanded} = this.state
return classnames(`confirm-button btn ${type} ${size}`, {
[customClass]: customClass,
'btn-square': square,
active: expanded,
disabled,
})
}
private get tooltipClassName(): string {
const {position} = this.props
if (position) {
return `confirm-button--tooltip ${position}`
}
if (!this.buttonDiv || !this.tooltipDiv) { if (!this.buttonDiv || !this.tooltipDiv) {
return '' return 'confirm-button--tooltip bottom'
} }
const windowWidth = window.innerWidth const windowWidth = window.innerWidth
@ -104,10 +115,10 @@ class ConfirmButton extends PureComponent<Props, State> {
const rightGap = windowWidth - buttonRect.right const rightGap = windowWidth - buttonRect.right
if (tooltipRect.width / 2 > rightGap) { if (tooltipRect.width / 2 > rightGap) {
return 'left' return 'confirm-button--tooltip left'
} }
return 'bottom' return 'confirm-button--tooltip bottom'
} }
} }

View File

@ -112,6 +112,13 @@ class NewAnnotation extends Component<Props, State> {
) )
} }
public handleClickOutside = () => {
const {onDismissAddingAnnotation, isTempHovering} = this.props
if (!isTempHovering) {
onDismissAddingAnnotation()
}
}
private clampWithinGraphTimerange = (timestamp: number): number => { private clampWithinGraphTimerange = (timestamp: number): number => {
const [xRangeStart] = this.props.dygraph.xAxisRange() const [xRangeStart] = this.props.dygraph.xAxisRange()
return Math.max(xRangeStart, timestamp) return Math.max(xRangeStart, timestamp)

View File

@ -11,18 +11,26 @@
position: absolute; position: absolute;
z-index: 1; z-index: 1;
&.top {
bottom: calc(100% + 4px);
left: 50%;
transform: translateX(-50%);
}
&.bottom { &.bottom {
top: calc(100% + 4px); top: calc(100% + 4px);
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
} }
&.left { &.left {
top: 50%; top: 50%;
right: calc(100% + 4px); right: calc(100% + 4px);
transform: translateY(-50%); transform: translateY(-50%);
} }
&.right {
top: 50%;
left: calc(100% + 4px);
transform: translateY(-50%);
}
} }
} }
.confirm-button--confirmation { .confirm-button--confirmation {
@ -43,11 +51,7 @@
&:after { &:after {
content: ''; content: '';
border: 8px solid transparent; border: 8px solid transparent;
border-bottom-color: $c-curacao;
position: absolute; position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
transition: border-color 0.25s ease; transition: border-color 0.25s ease;
z-index: 100; z-index: 100;
} }
@ -56,28 +60,35 @@
background-color: $c-dreamsicle; background-color: $c-dreamsicle;
cursor: pointer; cursor: pointer;
} }
&:hover:after {
border-bottom-color: $c-dreamsicle;
}
}
.confirm-button--tooltip.bottom .confirm-button--confirmation:after { .top &:after {
bottom: 100%; top: 100%;
left: 50%; left: 50%;
border-bottom-color: $c-curacao; transform: translateX(-50%);
transform: translateX(-50%); border-top-color: $c-curacao;
} }
.confirm-button--tooltip.bottom .confirm-button--confirmation:hover:after { .top &:hover:after {border-top-color: $c-dreamsicle;}
border-bottom-color: $c-dreamsicle; .bottom &:after {
} bottom: 100%;
.confirm-button--tooltip.left .confirm-button--confirmation:after { left: 50%;
left: 100%; transform: translateX(-50%);
top: 50%; border-bottom-color: $c-curacao;
border-left-color: $c-curacao; }
transform: translateY(-50%); .bottom &:hover:after {border-bottom-color: $c-dreamsicle;}
} .left &:after {
.confirm-button--tooltip.left .confirm-button--confirmation:hover:after { top: 50%;
border-left-color: $c-dreamsicle; left: 100%;
transform: translateY(-50%);
border-left-color: $c-curacao;
}
.left &:hover:after {border-left-color: $c-dreamsicle;}
.right &:after {
top: 50%;
right: 100%;
transform: translateY(-50%);
border-right-color: $c-curacao;
}
.right &:hover:after {border-right-color: $c-dreamsicle;}
} }
.confirm-button.active { .confirm-button.active {

View File

@ -144,6 +144,8 @@ $flux-invalid-hover: $c-dreamsicle;
&:hover, &:hover,
&.active { &.active {
cursor: pointer !important;
.func-node--preview { .func-node--preview {
color: $g20-white; color: $g20-white;
} }
@ -196,7 +198,7 @@ $flux-invalid-hover: $c-dreamsicle;
} }
// When a query exists unassigned to a variable // When a query exists unassigned to a variable
.func-node:first-child { .func-node--wrapper:first-child .func-node {
margin-left: 0; margin-left: 0;
padding-left: $flux-node-gap; padding-left: $flux-node-gap;
.func-node--connector { .func-node--connector {
@ -248,24 +250,26 @@ $flux-invalid-hover: $c-dreamsicle;
} }
} }
.func-node--wrapper {
display: flex;
align-items: center;
}
.func-node--menu { .func-node--menu {
display: flex; display: flex;
align-items: center; align-items: center;
position: absolute;
top: 50%;
right: 0;
transform: translate(100%, -50%);
opacity: 0; opacity: 0;
transition: opacity 0.25s ease; transition: opacity 0.25s ease;
> button.btn { > button.btn,
> .confirm-button {
margin-left: 4px; margin-left: 4px;
} }
} }
.func-node:hover .func-node--menu, .func-node--wrapper:hover .func-node--menu,
.func-node.editing .func-node--menu, .func-node.editing + .func-node--menu,
.func-node.active .func-node--menu { .func-node.active + .func-node--menu {
opacity: 1; opacity: 1;
} }

View File

@ -97,6 +97,7 @@ export const DEFAULT_TEMPLATES: DefaultTemplates = {
values: [], values: [],
type: TemplateType.CSV, type: TemplateType.CSV,
label: '', label: '',
query: {},
} }
}, },
[TemplateType.TagKeys]: () => { [TemplateType.TagKeys]: () => {

View File

@ -21,6 +21,7 @@ export type ScriptUpToYield = (
yieldNodeIndex: number, yieldNodeIndex: number,
isYieldable: boolean isYieldable: boolean
) => string ) => string
export type OnDeleteBody = (bodyID: string) => void
export interface ScriptStatus { export interface ScriptStatus {
type: string type: string
@ -105,6 +106,9 @@ export interface FlatBody {
funcs?: Func[] funcs?: Func[]
declarations?: FlatDeclaration[] declarations?: FlatDeclaration[]
} }
export interface Body extends FlatBody {
id: string
}
export interface Func { export interface Func {
type: string type: string

4
ui/src/types/promises.ts Normal file
View File

@ -0,0 +1,4 @@
export interface WrappedCancelablePromise<T> {
promise: Promise<T>
cancel: () => void
}

32
ui/src/utils/promises.ts Normal file
View File

@ -0,0 +1,32 @@
import {WrappedCancelablePromise} from 'src/types/promises'
export const makeCancelable = <T>(
promise: Promise<T>
): WrappedCancelablePromise<T> => {
let isCanceled = false
const wrappedPromise = new Promise<T>(async (resolve, reject) => {
try {
const value = await promise
if (isCanceled) {
reject({isCanceled})
} else {
resolve(value)
}
} catch (error) {
if (isCanceled) {
reject({isCanceled})
} else {
reject(error)
}
}
})
return {
promise: wrappedPromise,
cancel() {
isCanceled = true
},
}
}

View File

@ -15,6 +15,7 @@ const setup = () => {
onValidate: () => {}, onValidate: () => {},
onAppendFrom: () => {}, onAppendFrom: () => {},
onAppendJoin: () => {}, onAppendJoin: () => {},
onDeleteBody: () => {},
status: {type: '', text: ''}, status: {type: '', text: ''},
} }