diff --git a/CHANGELOG.md b/CHANGELOG.md index 1adffce4b48..f57f0a89b24 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## v2.0.0-alpha.7 [unreleased] ### Features +1. [12663](https://github.com/influxdata/influxdb/pull/12663): Insert flux function near cursor in flux editor ### Bug Fixes diff --git a/ui/src/shared/components/FluxEditor.tsx b/ui/src/shared/components/FluxEditor.tsx index 808fd982a5f..10928949195 100644 --- a/ui/src/shared/components/FluxEditor.tsx +++ b/ui/src/shared/components/FluxEditor.tsx @@ -1,7 +1,7 @@ // Libraries import React, {PureComponent} from 'react' import {Controlled as ReactCodeMirror, IInstance} from 'react-codemirror2' -import {EditorChange, LineWidget} from 'codemirror' +import {EditorChange, LineWidget, Position} from 'codemirror' import {ShowHintOptions} from 'src/types/codemirror' import 'src/external/codemirror' @@ -34,6 +34,7 @@ interface Props { onSubmitScript?: () => void suggestions: Suggestion[] visibility?: string + onCursorChange?: (position: Position) => void } interface Widget extends LineWidget { @@ -108,11 +109,20 @@ class FluxEditor extends PureComponent { onTouchStart={this.onTouchStart} editorDidMount={this.handleMount} onKeyUp={this.handleKeyUp} + onCursor={this.handleCursorChange} /> ) } + private handleCursorChange = (__: IInstance, position: Position) => { + const {onCursorChange} = this.props + + if (onCursorChange) { + onCursorChange(position) + } + } + private makeError(): void { this.editor.clearGutter('error-gutter') const lineNumbers = this.statusLine diff --git a/ui/src/timeMachine/components/TimeMachineFluxEditor.tsx b/ui/src/timeMachine/components/TimeMachineFluxEditor.tsx index ddf42ab5411..a827c4ec02c 100644 --- a/ui/src/timeMachine/components/TimeMachineFluxEditor.tsx +++ b/ui/src/timeMachine/components/TimeMachineFluxEditor.tsx @@ -1,6 +1,7 @@ // Libraries import React, {PureComponent} from 'react' import {connect} from 'react-redux' +import {Position} from 'codemirror' // Components import FluxEditor from 'src/shared/components/FluxEditor' @@ -15,6 +16,7 @@ import {saveAndExecuteQueries} from 'src/timeMachine/actions/queries' // Utils import {getActiveQuery} from 'src/timeMachine/selectors' +import {insertFluxFunction} from 'src/timeMachine/utils/scriptInsertion' // Constants import {HANDLE_VERTICAL, HANDLE_NONE} from 'src/shared/constants' @@ -42,6 +44,8 @@ interface State { type Props = StateProps & DispatchProps class TimeMachineFluxEditor extends PureComponent { + private cursorPosition: Position = {line: 0, ch: 0} + public state: State = { displayFluxFunctions: true, } @@ -60,6 +64,7 @@ class TimeMachineFluxEditor extends PureComponent { onChangeScript={onSetActiveQueryText} onSubmitScript={onSubmitQueries} suggestions={[]} + onCursorChange={this.handleCursorPosition} /> ), }, @@ -102,12 +107,38 @@ class TimeMachineFluxEditor extends PureComponent { const {displayFluxFunctions} = this.state if (displayFluxFunctions) { - return + return ( + + ) } return } + private handleCursorPosition = (position: Position): void => { + this.cursorPosition = position + } + + private handleInsertFluxFunction = async ( + functionName: string, + fluxFunction: string + ): Promise => { + const {activeQueryText} = this.props + const {line} = this.cursorPosition + + const {updatedScript, cursorPosition} = insertFluxFunction( + line, + activeQueryText, + functionName, + fluxFunction + ) + await this.props.onSetActiveQueryText(updatedScript) + + this.handleCursorPosition(cursorPosition) + } + private showFluxFunctions = () => { this.setState({displayFluxFunctions: true}) } diff --git a/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.tsx b/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.tsx index 1375c4577ca..bb1c2637c93 100644 --- a/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.tsx +++ b/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.tsx @@ -16,7 +16,7 @@ import {setActiveQueryText} from 'src/timeMachine/actions' import {getActiveQuery} from 'src/timeMachine/selectors' // Constants -import {FLUX_FUNCTIONS, FROM, UNION} from 'src/shared/constants/fluxFunctions' +import {FLUX_FUNCTIONS} from 'src/shared/constants/fluxFunctions' // Styles import 'src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss' @@ -24,6 +24,10 @@ import 'src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scs // Types import {AppState} from 'src/types/v2' +interface OwnProps { + onInsertFluxFunction: (functionName: string, text: string) => void +} + interface StateProps { activeQueryText: string } @@ -32,7 +36,7 @@ interface DispatchProps { onSetActiveQueryText: (script: string) => void } -type Props = StateProps & DispatchProps +type Props = OwnProps & StateProps & DispatchProps interface State { searchTerm: string @@ -59,7 +63,7 @@ class FluxFunctionsToolbar extends PureComponent { key={category} category={category} funcs={funcs} - onClickFunction={this.handleUpdateScript} + onClickFunction={this.handleClickFunction} /> )) } @@ -74,21 +78,8 @@ class FluxFunctionsToolbar extends PureComponent { this.setState({searchTerm}) } - private handleUpdateScript = (funcName: string, funcExample: string) => { - const {activeQueryText, onSetActiveQueryText} = this.props - - switch (funcName) { - case FROM.name: { - onSetActiveQueryText(`${activeQueryText}\n${funcExample}`) - return - } - case UNION.name: { - onSetActiveQueryText(`${activeQueryText.trimRight()}\n\n${funcExample}`) - return - } - default: - onSetActiveQueryText(`${activeQueryText}\n |> ${funcExample}`) - } + private handleClickFunction = (funcName: string, funcExample: string) => { + this.props.onInsertFluxFunction(funcName, funcExample) } } diff --git a/ui/src/timeMachine/utils/scriptInsertion.ts b/ui/src/timeMachine/utils/scriptInsertion.ts new file mode 100644 index 00000000000..6b335d22d68 --- /dev/null +++ b/ui/src/timeMachine/utils/scriptInsertion.ts @@ -0,0 +1,98 @@ +import {Position} from 'codemirror' + +// Constants +import {FROM, UNION} from 'src/shared/constants/fluxFunctions' + +const rejoinScript = (scriptLines: string[]): string => { + return scriptLines.join('\n') +} + +const insertAtLine = ( + lineNumber: number, + scriptLines: string[], + textToInsert: string, + insertOnSameLine?: boolean +): string => { + const front = scriptLines.slice(0, lineNumber) + + const backStartIndex = insertOnSameLine ? lineNumber + 1 : lineNumber + const back = scriptLines.slice(backStartIndex) + + const updated = [...front, textToInsert, ...back] + + return rejoinScript(updated) +} + +const getInsertLineNumber = ( + currentLineNumber: number, + scriptLines: string[] +): number => { + const currentLine = scriptLines[currentLineNumber] + + // Insert on the current line if its an empty line + if (!currentLine.trim()) { + return currentLineNumber + } + + return currentLineNumber + 1 +} + +const functionRequiresNewLine = (funcName: string): boolean => { + switch (funcName) { + case FROM.name: + case UNION.name: { + return true + } + default: + return false + } +} + +const formatFunctionForInsert = (funcName: string, fluxFunction: string) => { + if (functionRequiresNewLine(funcName)) { + return `\n${fluxFunction}` + } + + return ` |> ${fluxFunction}` +} + +const getCursorPosition = ( + insertLineNumber, + formattedFunction, + funcName +): Position => { + const ch = formattedFunction.length - 1 + const line = functionRequiresNewLine(funcName) + ? insertLineNumber + 1 + : insertLineNumber + + return {line, ch} +} + +export const insertFluxFunction = ( + currentLineNumber: number, + currentScript: string, + functionName: string, + fluxFunction: string +): {updatedScript: string; cursorPosition: Position} => { + const scriptLines = currentScript.split('\n') + const insertLineNumber = getInsertLineNumber(currentLineNumber, scriptLines) + const insertOnSameLine = currentLineNumber === insertLineNumber + + const formattedFunction = formatFunctionForInsert(functionName, fluxFunction) + + const updatedScript = insertAtLine( + insertLineNumber, + scriptLines, + formattedFunction, + insertOnSameLine + ) + + const updatedCursorPosition = getCursorPosition( + insertLineNumber, + formattedFunction, + functionName + ) + + return {updatedScript, cursorPosition: updatedCursorPosition} +}