diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 000000000..56c4969b9 --- /dev/null +++ b/.circleci/config.yml @@ -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/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d7df20542..6d941200d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ 1. [#3474](https://github.com/influxdata/chronograf/pull/3474): Sort task table on Manage Alert page alphabetically 1. [#3590](https://github.com/influxdata/chronograf/pull/3590): Redesign icons in side navigation +1. [#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. [#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 diff --git a/circle.yml b/circle.yml deleted file mode 100644 index 279d238e8..000000000 --- a/circle.yml +++ /dev/null @@ -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 diff --git a/ui/src/dashboards/components/QueryTextArea.js b/ui/src/dashboards/components/QueryTextArea.js index ea152f8cd..655ef51e8 100644 --- a/ui/src/dashboards/components/QueryTextArea.js +++ b/ui/src/dashboards/components/QueryTextArea.js @@ -12,7 +12,7 @@ import { applyMasks, insertTempVar, unMask, -} from 'src/dashboards/constants' +} from 'src/tempVars/constants' @ErrorHandling class QueryTextArea extends Component { diff --git a/ui/src/dashboards/utils/tempVars.ts b/ui/src/dashboards/utils/tempVars.ts index d122b8aed..1bb1544b8 100644 --- a/ui/src/dashboards/utils/tempVars.ts +++ b/ui/src/dashboards/utils/tempVars.ts @@ -32,7 +32,7 @@ export const generateURLQueryParamsFromTempVars = ( const selected = values.find(value => value.selected === true) const strippedTempVar = stripTempVar(tempVar) - urlQueryParams[strippedTempVar] = selected.value + urlQueryParams[strippedTempVar] = _.get(selected, 'value', '') }) return urlQueryParams diff --git a/ui/src/flux/components/BodyBuilder.tsx b/ui/src/flux/components/BodyBuilder.tsx index f182dd9f6..faa31eb32 100644 --- a/ui/src/flux/components/BodyBuilder.tsx +++ b/ui/src/flux/components/BodyBuilder.tsx @@ -3,12 +3,13 @@ import _ from 'lodash' import FancyScrollbar from 'src/shared/components/FancyScrollbar' 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 BodyDelete from 'src/flux/components/BodyDelete' import {funcNames} from 'src/flux/constants' import {Service} from 'src/types' -import {FlatBody, Suggestion} from 'src/types/flux' +import {Body, Suggestion} from 'src/types/flux' interface Props { service: Service @@ -16,21 +17,25 @@ interface Props { suggestions: Suggestion[] onAppendFrom: () => void onAppendJoin: () => void -} - -interface Body extends FlatBody { - id: string + onDeleteBody: (bodyID: string) => void } class BodyBuilder extends PureComponent { 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) { return b.declarations.map(d => { if (d.funcs) { return (
- +
+ +
+ +
+
{ funcs={d.funcs} declarationsFromBody={this.declarationsFromBody} isLastBody={this.isLastBody(i)} + onDeleteBody={onDeleteBody} />
) @@ -45,7 +51,16 @@ class BodyBuilder extends PureComponent { return (
- +
+ +
+ +
+
) }) @@ -59,6 +74,7 @@ class BodyBuilder extends PureComponent { funcNames={this.funcNames} declarationsFromBody={this.declarationsFromBody} isLastBody={this.isLastBody(i)} + onDeleteBody={onDeleteBody} /> ) diff --git a/ui/src/flux/components/BodyDelete.tsx b/ui/src/flux/components/BodyDelete.tsx new file mode 100644 index 000000000..d5f6c0461 --- /dev/null +++ b/ui/src/flux/components/BodyDelete.tsx @@ -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 { + public static defaultProps: Partial = { + type: 'query', + } + + public render() { + const {type} = this.props + + if (type === 'variable') { + return ( + + ) + } + + return ( + + ) + } + + private handleDelete = (): void => { + this.props.onDeleteBody(this.props.bodyID) + } +} + +export default BodyDelete diff --git a/ui/src/flux/components/ExpressionNode.tsx b/ui/src/flux/components/ExpressionNode.tsx index b70d4f4aa..8275fd6cf 100644 --- a/ui/src/flux/components/ExpressionNode.tsx +++ b/ui/src/flux/components/ExpressionNode.tsx @@ -15,6 +15,7 @@ interface Props { declarationID?: string declarationsFromBody: string[] isLastBody: boolean + onDeleteBody: (bodyID: string) => void } interface State { @@ -42,6 +43,7 @@ class ExpressionNode extends PureComponent { funcNames, funcs, declarationsFromBody, + onDeleteBody, } = this.props const {nonYieldableIndexesToggled} = this.state @@ -106,6 +108,7 @@ class ExpressionNode extends PureComponent { onGenerateScript={onGenerateScript} declarationsFromBody={declarationsFromBody} onToggleYieldWithLast={this.handleToggleYieldWithLast} + onDeleteBody={onDeleteBody} /> ) @@ -152,6 +155,7 @@ class ExpressionNode extends PureComponent { onGenerateScript={onGenerateScript} declarationsFromBody={declarationsFromBody} onToggleYieldWithLast={this.handleToggleYieldWithLast} + onDeleteBody={onDeleteBody} /> { + private fetchTagKeysResponse?: WrappedCancelablePromise + constructor(props) { super(props) this.state = { @@ -52,15 +56,24 @@ class FilterArgs extends PureComponent { } public async componentDidMount() { - const {db, service} = this.props - try { this.convertStringToNodes() - const response = await fetchTagKeys(service, db, []) + const response = await this.getTagKeys() const tagKeys = parseValuesColumn(response) - this.setState({tagKeys}) + + this.setState({ + tagKeys, + }) } 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 { /> ) } + + private getTagKeys(): Promise { + const {db, service} = this.props + + this.fetchTagKeysResponse = makeCancelable(fetchTagKeys(service, db, [])) + + return this.fetchTagKeysResponse.promise + } } const mapStateToProps = ({links}) => { diff --git a/ui/src/flux/components/FuncNode.tsx b/ui/src/flux/components/FuncNode.tsx index 84594a233..0eeefd076 100644 --- a/ui/src/flux/components/FuncNode.tsx +++ b/ui/src/flux/components/FuncNode.tsx @@ -2,6 +2,7 @@ import React, {PureComponent, MouseEvent} from 'react' import classnames from 'classnames' import _ from 'lodash' +import BodyDelete from 'src/flux/components/BodyDelete' import FuncArgs from 'src/flux/components/FuncArgs' import FuncArgsPreview from 'src/flux/components/FuncArgsPreview' import { @@ -28,6 +29,7 @@ interface Props { declarationsFromBody: string[] isYielding: boolean isYieldable: boolean + onDeleteBody: (bodyID: string) => void } interface State { @@ -53,14 +55,16 @@ export default class FuncNode extends PureComponent { return ( <> -
-
-
{func.name}
- +
+
+
+
{func.name}
+ +
{this.funcMenu}
{this.funcArgs} @@ -103,13 +107,7 @@ export default class FuncNode extends PureComponent { return (
{this.yieldToggleButton} - + {this.deleteButton}
) } @@ -140,6 +138,24 @@ export default class FuncNode extends PureComponent { ) } + private get deleteButton(): JSX.Element { + const {func, bodyID, onDeleteBody} = this.props + + if (func.name === 'from') { + return + } + + return ( + + ) + } + private get nodeClassName(): string { const {isYielding} = this.props const {editing} = this.state @@ -147,14 +163,14 @@ export default class FuncNode extends PureComponent { return classnames('func-node', {active: isYielding || editing}) } - private handleDelete = (e: MouseEvent): void => { - e.stopPropagation() + private handleDelete = (): void => { const {func, bodyID, declarationID} = this.props this.props.onDelete({funcID: func.id, bodyID, declarationID}) } - private handleToggleEdit = (): void => { + private handleToggleEdit = (e: MouseEvent): void => { + e.stopPropagation() this.setState({editing: !this.state.editing}) } diff --git a/ui/src/flux/components/TimeMachine.tsx b/ui/src/flux/components/TimeMachine.tsx index b92368654..45556135f 100644 --- a/ui/src/flux/components/TimeMachine.tsx +++ b/ui/src/flux/components/TimeMachine.tsx @@ -7,6 +7,7 @@ import { Suggestion, OnChangeScript, OnSubmitScript, + OnDeleteBody, FlatBody, ScriptStatus, } from 'src/types/flux' @@ -22,6 +23,7 @@ interface Props { status: ScriptStatus suggestions: Suggestion[] onChangeScript: OnChangeScript + onDeleteBody: OnDeleteBody onSubmitScript: OnSubmitScript onAppendFrom: () => void onAppendJoin: () => void @@ -63,7 +65,14 @@ class TimeMachine extends PureComponent { } private get builder() { - const {body, service, suggestions, onAppendFrom, onAppendJoin} = this.props + const { + body, + service, + suggestions, + onAppendFrom, + onDeleteBody, + onAppendJoin, + } = this.props return { name: 'Build', @@ -75,6 +84,7 @@ class TimeMachine extends PureComponent { body={body} service={service} suggestions={suggestions} + onDeleteBody={onDeleteBody} onAppendFrom={onAppendFrom} onAppendJoin={onAppendJoin} /> diff --git a/ui/src/flux/components/VariableName.tsx b/ui/src/flux/components/VariableNode.tsx similarity index 100% rename from ui/src/flux/components/VariableName.tsx rename to ui/src/flux/components/VariableNode.tsx diff --git a/ui/src/flux/constants/ast.ts b/ui/src/flux/constants/ast.ts index e542d8b43..30822212a 100644 --- a/ui/src/flux/constants/ast.ts +++ b/ui/src/flux/constants/ast.ts @@ -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 = { type: 'File', start: 0, diff --git a/ui/src/flux/constants/index.ts b/ui/src/flux/constants/index.ts index 81695e650..b81ebaa08 100644 --- a/ui/src/flux/constants/index.ts +++ b/ui/src/flux/constants/index.ts @@ -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 argTypes from 'src/flux/constants/argumentTypes' import * as funcNames from 'src/flux/constants/funcNames' @@ -10,6 +10,7 @@ const MAX_RESPONSE_BYTES = 1e7 // 10 MB export { ast, + emptyAST, funcNames, argTypes, editor, diff --git a/ui/src/flux/containers/FluxPage.tsx b/ui/src/flux/containers/FluxPage.tsx index 11b9ded99..24be29632 100644 --- a/ui/src/flux/containers/FluxPage.tsx +++ b/ui/src/flux/containers/FluxPage.tsx @@ -15,7 +15,7 @@ import {UpdateScript} from 'src/flux/actions' import {bodyNodes} from 'src/flux/helpers' 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 { @@ -114,6 +114,7 @@ export class FluxPage extends PureComponent { onAppendJoin={this.handleAppendJoin} onChangeScript={this.handleChangeScript} onSubmitScript={this.handleSubmitScript} + onDeleteBody={this.handleDeleteBody} />
@@ -331,6 +332,13 @@ export class FluxPage extends PureComponent { 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, @@ -601,7 +609,8 @@ export class FluxPage extends PureComponent { const {links} = this.props if (!script) { - return + this.props.updateScript(script) + return this.setState({ast: emptyAST, body: []}) } try { diff --git a/ui/src/shared/components/AnnotationInput.tsx b/ui/src/shared/components/AnnotationInput.tsx index 8d8129048..58e766ca7 100644 --- a/ui/src/shared/components/AnnotationInput.tsx +++ b/ui/src/shared/components/AnnotationInput.tsx @@ -45,6 +45,10 @@ class AnnotationInput extends Component {
) } + public handleClickOutside = () => { + this.props.onConfirmUpdate() + this.setState({isEditing: false}) + } private handleInputClick = () => { this.setState({isEditing: true}) diff --git a/ui/src/shared/components/ConfirmButton.tsx b/ui/src/shared/components/ConfirmButton.tsx index c211b3990..4674697be 100644 --- a/ui/src/shared/components/ConfirmButton.tsx +++ b/ui/src/shared/components/ConfirmButton.tsx @@ -1,7 +1,10 @@ import React, {PureComponent} from 'react' +import classnames from 'classnames' import {ClickOutside} from 'src/shared/components/ClickOutside' import {ErrorHandling} from 'src/shared/decorators/errors' +type Position = 'top' | 'bottom' | 'left' | 'right' + interface Props { text?: string confirmText?: string @@ -12,6 +15,7 @@ interface Props { icon?: string disabled?: boolean customClass?: string + position?: Position } interface State { @@ -47,10 +51,11 @@ class ConfirmButton extends PureComponent { className={this.className} onClick={this.handleButtonClick} ref={r => (this.buttonDiv = r)} + title={confirmText} > {icon && } {text && text} -
+
{ ) } - 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 = () => { if (this.props.disabled) { return @@ -92,9 +85,27 @@ class ConfirmButton extends PureComponent { 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) { - return '' + return 'confirm-button--tooltip bottom' } const windowWidth = window.innerWidth @@ -104,10 +115,10 @@ class ConfirmButton extends PureComponent { const rightGap = windowWidth - buttonRect.right if (tooltipRect.width / 2 > rightGap) { - return 'left' + return 'confirm-button--tooltip left' } - return 'bottom' + return 'confirm-button--tooltip bottom' } } diff --git a/ui/src/shared/components/NewAnnotation.tsx b/ui/src/shared/components/NewAnnotation.tsx index a06e4dda6..f1436eac5 100644 --- a/ui/src/shared/components/NewAnnotation.tsx +++ b/ui/src/shared/components/NewAnnotation.tsx @@ -112,6 +112,13 @@ class NewAnnotation extends Component { ) } + public handleClickOutside = () => { + const {onDismissAddingAnnotation, isTempHovering} = this.props + if (!isTempHovering) { + onDismissAddingAnnotation() + } + } + private clampWithinGraphTimerange = (timestamp: number): number => { const [xRangeStart] = this.props.dygraph.xAxisRange() return Math.max(xRangeStart, timestamp) diff --git a/ui/src/style/components/confirm-button.scss b/ui/src/style/components/confirm-button.scss index 74bc7a193..a2b1280b0 100644 --- a/ui/src/style/components/confirm-button.scss +++ b/ui/src/style/components/confirm-button.scss @@ -11,18 +11,26 @@ position: absolute; z-index: 1; - + &.top { + bottom: calc(100% + 4px); + left: 50%; + transform: translateX(-50%); + } &.bottom { top: calc(100% + 4px); left: 50%; transform: translateX(-50%); } - &.left { top: 50%; right: calc(100% + 4px); transform: translateY(-50%); } + &.right { + top: 50%; + left: calc(100% + 4px); + transform: translateY(-50%); + } } } .confirm-button--confirmation { @@ -43,11 +51,7 @@ &:after { content: ''; border: 8px solid transparent; - border-bottom-color: $c-curacao; position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); transition: border-color 0.25s ease; z-index: 100; } @@ -56,28 +60,35 @@ background-color: $c-dreamsicle; cursor: pointer; } - &:hover:after { - border-bottom-color: $c-dreamsicle; - } -} -.confirm-button--tooltip.bottom .confirm-button--confirmation:after { - bottom: 100%; - left: 50%; - border-bottom-color: $c-curacao; - transform: translateX(-50%); -} -.confirm-button--tooltip.bottom .confirm-button--confirmation:hover:after { - border-bottom-color: $c-dreamsicle; -} -.confirm-button--tooltip.left .confirm-button--confirmation:after { - left: 100%; - top: 50%; - border-left-color: $c-curacao; - transform: translateY(-50%); -} -.confirm-button--tooltip.left .confirm-button--confirmation:hover:after { - border-left-color: $c-dreamsicle; + .top &:after { + top: 100%; + left: 50%; + transform: translateX(-50%); + border-top-color: $c-curacao; + } + .top &:hover:after {border-top-color: $c-dreamsicle;} + .bottom &:after { + bottom: 100%; + left: 50%; + transform: translateX(-50%); + border-bottom-color: $c-curacao; + } + .bottom &:hover:after {border-bottom-color: $c-dreamsicle;} + .left &:after { + top: 50%; + 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 { diff --git a/ui/src/style/components/time-machine/flux-builder.scss b/ui/src/style/components/time-machine/flux-builder.scss index 6e7e2925c..7866f92c3 100644 --- a/ui/src/style/components/time-machine/flux-builder.scss +++ b/ui/src/style/components/time-machine/flux-builder.scss @@ -144,6 +144,8 @@ $flux-invalid-hover: $c-dreamsicle; &:hover, &.active { + cursor: pointer !important; + .func-node--preview { color: $g20-white; } @@ -196,7 +198,7 @@ $flux-invalid-hover: $c-dreamsicle; } // When a query exists unassigned to a variable -.func-node:first-child { +.func-node--wrapper:first-child .func-node { margin-left: 0; padding-left: $flux-node-gap; .func-node--connector { @@ -248,24 +250,26 @@ $flux-invalid-hover: $c-dreamsicle; } } +.func-node--wrapper { + display: flex; + align-items: center; +} + .func-node--menu { display: flex; align-items: center; - position: absolute; - top: 50%; - right: 0; - transform: translate(100%, -50%); opacity: 0; transition: opacity 0.25s ease; - > button.btn { + > button.btn, + > .confirm-button { margin-left: 4px; } } -.func-node:hover .func-node--menu, -.func-node.editing .func-node--menu, -.func-node.active .func-node--menu { +.func-node--wrapper:hover .func-node--menu, +.func-node.editing + .func-node--menu, +.func-node.active + .func-node--menu { opacity: 1; } diff --git a/ui/src/tempVars/constants/index.ts b/ui/src/tempVars/constants/index.ts index 909cf8e0e..6fe11741c 100644 --- a/ui/src/tempVars/constants/index.ts +++ b/ui/src/tempVars/constants/index.ts @@ -97,6 +97,7 @@ export const DEFAULT_TEMPLATES: DefaultTemplates = { values: [], type: TemplateType.CSV, label: '', + query: {}, } }, [TemplateType.TagKeys]: () => { diff --git a/ui/src/types/flux.ts b/ui/src/types/flux.ts index 88014180c..ccfae353f 100644 --- a/ui/src/types/flux.ts +++ b/ui/src/types/flux.ts @@ -21,6 +21,7 @@ export type ScriptUpToYield = ( yieldNodeIndex: number, isYieldable: boolean ) => string +export type OnDeleteBody = (bodyID: string) => void export interface ScriptStatus { type: string @@ -105,6 +106,9 @@ export interface FlatBody { funcs?: Func[] declarations?: FlatDeclaration[] } +export interface Body extends FlatBody { + id: string +} export interface Func { type: string diff --git a/ui/src/types/promises.ts b/ui/src/types/promises.ts new file mode 100644 index 000000000..765e1e388 --- /dev/null +++ b/ui/src/types/promises.ts @@ -0,0 +1,4 @@ +export interface WrappedCancelablePromise { + promise: Promise + cancel: () => void +} diff --git a/ui/src/utils/promises.ts b/ui/src/utils/promises.ts new file mode 100644 index 000000000..4987984ad --- /dev/null +++ b/ui/src/utils/promises.ts @@ -0,0 +1,32 @@ +import {WrappedCancelablePromise} from 'src/types/promises' + +export const makeCancelable = ( + promise: Promise +): WrappedCancelablePromise => { + let isCanceled = false + + const wrappedPromise = new Promise(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 + }, + } +} diff --git a/ui/test/flux/components/TimeMachine.test.tsx b/ui/test/flux/components/TimeMachine.test.tsx index f701c5cfa..2fa66c20a 100644 --- a/ui/test/flux/components/TimeMachine.test.tsx +++ b/ui/test/flux/components/TimeMachine.test.tsx @@ -15,6 +15,7 @@ const setup = () => { onValidate: () => {}, onAppendFrom: () => {}, onAppendJoin: () => {}, + onDeleteBody: () => {}, status: {type: '', text: ''}, }