diff --git a/CHANGELOG.md b/CHANGELOG.md index d7df20542e..6d941200d2 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/ui/src/flux/components/BodyBuilder.tsx b/ui/src/flux/components/BodyBuilder.tsx index f182dd9f6f..faa31eb32a 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 0000000000..d5f6c0461b --- /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 b70d4f4aaf..8275fd6cf7 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} /> 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 b923686549..45556135fd 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 e542d8b432..30822212a5 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 81695e650d..b81ebaa087 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 11b9ded99d..24be296328 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/ConfirmButton.tsx b/ui/src/shared/components/ConfirmButton.tsx index c211b3990f..4674697be2 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/style/components/confirm-button.scss b/ui/src/style/components/confirm-button.scss index 74bc7a1935..a2b1280b08 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 6e7e2925ce..7866f92c37 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/types/flux.ts b/ui/src/types/flux.ts index 88014180c2..ccfae353f1 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/test/flux/components/TimeMachine.test.tsx b/ui/test/flux/components/TimeMachine.test.tsx index f701c5cfa6..2fa66c20a7 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: ''}, }