Merge pull request #3696 from influxdata/flux/deletion

Introduce ability to delete entire queries
pull/10616/head
Alex Paxton 2018-06-18 21:23:18 -07:00 committed by GitHub
commit 4a166b9706
15 changed files with 237 additions and 84 deletions

View File

@ -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

View File

@ -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<Props> {
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 (
<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
bodyID={b.id}
declarationID={d.id}
@ -38,6 +43,7 @@ class BodyBuilder extends PureComponent<Props> {
funcs={d.funcs}
declarationsFromBody={this.declarationsFromBody}
isLastBody={this.isLastBody(i)}
onDeleteBody={onDeleteBody}
/>
</div>
)
@ -45,7 +51,16 @@ class BodyBuilder extends PureComponent<Props> {
return (
<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>
)
})
@ -59,6 +74,7 @@ class BodyBuilder extends PureComponent<Props> {
funcNames={this.funcNames}
declarationsFromBody={this.declarationsFromBody}
isLastBody={this.isLastBody(i)}
onDeleteBody={onDeleteBody}
/>
</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
declarationsFromBody: string[]
isLastBody: boolean
onDeleteBody: (bodyID: string) => void
}
interface State {
@ -42,6 +43,7 @@ class ExpressionNode extends PureComponent<Props, State> {
funcNames,
funcs,
declarationsFromBody,
onDeleteBody,
} = this.props
const {nonYieldableIndexesToggled} = this.state
@ -106,6 +108,7 @@ class ExpressionNode extends PureComponent<Props, State> {
onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody}
onToggleYieldWithLast={this.handleToggleYieldWithLast}
onDeleteBody={onDeleteBody}
/>
)
@ -152,6 +155,7 @@ class ExpressionNode extends PureComponent<Props, State> {
onGenerateScript={onGenerateScript}
declarationsFromBody={declarationsFromBody}
onToggleYieldWithLast={this.handleToggleYieldWithLast}
onDeleteBody={onDeleteBody}
/>
<YieldFuncNode
index={i}

View File

@ -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<Props, State> {
return (
<>
<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 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}
@ -103,13 +107,7 @@ export default class FuncNode extends PureComponent<Props, State> {
return (
<div className="func-node--menu">
{this.yieldToggleButton}
<button
className="btn btn-sm btn-square btn-danger"
onClick={this.handleDelete}
title="Delete this Function"
>
<span className="icon trash" />
</button>
{this.deleteButton}
</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 {
const {isYielding} = this.props
const {editing} = this.state
@ -147,14 +163,14 @@ export default class FuncNode extends PureComponent<Props, State> {
return classnames('func-node', {active: isYielding || editing})
}
private handleDelete = (e: MouseEvent<HTMLElement>): 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<HTMLElement>): void => {
e.stopPropagation()
this.setState({editing: !this.state.editing})
}

View File

@ -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<Props> {
}
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<Props> {
body={body}
service={service}
suggestions={suggestions}
onDeleteBody={onDeleteBody}
onAppendFrom={onAppendFrom}
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 = {
type: 'File',
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 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,

View File

@ -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<Props, State> {
onAppendJoin={this.handleAppendJoin}
onChangeScript={this.handleChangeScript}
onSubmitScript={this.handleSubmitScript}
onDeleteBody={this.handleDeleteBody}
/>
</div>
</KeyboardShortcuts>
@ -331,6 +332,13 @@ export class FluxPage extends PureComponent<Props, State> {
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<Props, State> {
const {links} = this.props
if (!script) {
return
this.props.updateScript(script)
return this.setState({ast: emptyAST, body: []})
}
try {

View File

@ -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<Props, State> {
className={this.className}
onClick={this.handleButtonClick}
ref={r => (this.buttonDiv = r)}
title={confirmText}
>
{icon && <span className={`icon ${icon}`} />}
{text && text}
<div className={`confirm-button--tooltip ${this.calculatedPosition}`}>
<div className={this.tooltipClassName}>
<div
className="confirm-button--confirmation"
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 = () => {
if (this.props.disabled) {
return
@ -92,9 +85,27 @@ class ConfirmButton extends PureComponent<Props, State> {
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<Props, State> {
const rightGap = windowWidth - buttonRect.right
if (tooltipRect.width / 2 > rightGap) {
return 'left'
return 'confirm-button--tooltip left'
}
return 'bottom'
return 'confirm-button--tooltip bottom'
}
}

View File

@ -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 {

View File

@ -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;
}

View File

@ -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

View File

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