diff --git a/ui/cypress/e2e/explorer.test.ts b/ui/cypress/e2e/explorer.test.ts index 2fbaedc4d1..74f46b66b4 100644 --- a/ui/cypress/e2e/explorer.test.ts +++ b/ui/cypress/e2e/explorer.test.ts @@ -498,9 +498,9 @@ describe('DataExplorer', () => { .click() .focused() .type( - `from(bucket: "defbuck") - |> range(start: -10s) - |> filter(fn: (r) => r._measurement == "no exist")`, + `from(bucket: "defbuck"{rightarrow} + |> range(start: -10s{rightarrow} + |> filter(fn: (r{rightarrow} => r._measurement == "no exist"{rightarrow}`, {force: true, delay: TYPE_DELAY} ) cy.getByTestID('time-machine-submit-button').click() @@ -518,9 +518,9 @@ describe('DataExplorer', () => { .click() .focused() .type( - `from(bucket: "defbuck") - |> range(start: -15m, stop: now()) - |> filter(fn: (r) => r._measurement == `, + `from(bucket: "defbuck"{rightarrow} + |> range(start: -15m, stop: now({rightarrow}{rightarrow} + |> filter(fn: (r{rightarrow} => r._measurement ==`, {force: true, delay: TYPE_DELAY} ) }) @@ -528,14 +528,6 @@ describe('DataExplorer', () => { cy.getByTestID('toolbar-tab').click() //insert variable name by clicking on variable cy.get('.variables-toolbar--label').click() - // finish flux - cy.getByTestID('flux-editor').within(() => { - cy.get('.react-monaco-editor-container') - .should('exist') - .click() - .focused() - .type(`)`, {force: true, delay: TYPE_DELAY}) - }) cy.getByTestID('save-query-as').click() cy.getByTestID('task--radio-button').click() diff --git a/ui/cypress/e2e/tasks.test.ts b/ui/cypress/e2e/tasks.test.ts index bb84b2461e..0397f5ca8d 100644 --- a/ui/cypress/e2e/tasks.test.ts +++ b/ui/cypress/e2e/tasks.test.ts @@ -28,11 +28,11 @@ describe('Tasks', () => { const taskName = 'Bad Task' createFirstTask(taskName, ({name}) => { - return `import "influxdata/influxdb/v1" -v1.tagValues(bucket: "${name}", tag: "_field") -from(bucket: "${name}") - |> range(start: -2m) - |> to(org: "${name}")` + return `import "influxdata/influxdb/v1{rightarrow} +v1.tagValues(bucket: "${name}", tag: "_field"{rightarrow} +from(bucket: "${name}"{rightarrow} + |> range(start: -2m{rightarrow} + |> to(org: "${name}"{rightarrow}` }) cy.getByTestID('task-save-btn').click() @@ -46,10 +46,10 @@ from(bucket: "${name}") it('can create a task', () => { const taskName = 'Task' createFirstTask(taskName, ({name}) => { - return `import "influxdata/influxdb/v1" -v1.tagValues(bucket: "${name}", tag: "_field") -from(bucket: "${name}") - |> range(start: -2m)` + return `import "influxdata/influxdb/v1{rightarrow} +v1.tagValues(bucket: "${name}", tag: "_field"{rightarrow} +from(bucket: "${name}"{rightarrow} + |> range(start: -2m{rightarrow}` }) cy.getByTestID('task-save-btn').click() @@ -62,11 +62,11 @@ from(bucket: "${name}") it.skip('can create a task using http.post', () => { const taskName = 'Task' createFirstTask(taskName, () => { - return `import "http" + return `import "http{rightarrow} http.post( url: "https://foo.bar/baz", - data: bytes(v: "body") -)` + data: bytes(v: "body"{rightarrow} + {rightarrow}` }) cy.getByTestID('task-save-btn').click() @@ -229,10 +229,10 @@ http.post( createFirstTask( taskName, ({name}) => { - return `import "influxdata/influxdb/v1" - v1.tagValues(bucket: "${name}", tag: "_field") - from(bucket: "${name}") - |> range(start: -2m)` + return `import "influxdata/influxdb/v1{rightarrow} + v1.tagValues(bucket: "${name}", tag: "_field"{rightarrow} + from(bucket: "${name}"{rightarrow} + |> range(start: -2m{rightarrow}` }, interval, offset @@ -304,55 +304,22 @@ http.post( // https://github.com/influxdata/influxdb/issues/15552 const firstTask = 'First_Task' const secondTask = 'Second_Task' - const interval = '12h' - const offset = '30m' - const flux = name => `import "influxdata/influxdb/v1" - v1.tagValues(bucket: "${name}", tag: "_field") - from(bucket: "${name}") - |> range(start: -2m)` beforeEach(() => { - createFirstTask( - firstTask, - ({name}) => { - return flux(name) - }, - interval, - offset - ) - cy.getByTestID('task-save-btn').click() - cy.getByTestID('task-card') - .should('have.length', 1) - .and('contain', firstTask) - - cy.getByTestID('add-resource-dropdown--button').click() - cy.getByTestID('add-resource-dropdown--new').click() - cy.getByInputName('name').type(secondTask) - cy.getByTestID('task-form-schedule-input').type(interval) - cy.getByTestID('task-form-offset-input').type(offset) - cy.get('@bucket').then(bucket => { - cy.getByTestID('flux-editor').within(() => { - cy.get('.react-monaco-editor-container') - .should('be.visible') - .click() - .focused() - .type(flux(bucket), {force: true, delay: 2}) + cy.get('@org').then(({id}: Organization) => { + cy.get('@token').then(token => { + cy.createTask(token, id, firstTask) + cy.createTask(token, id, secondTask) + }) + }) + + cy.fixture('routes').then(({orgs}) => { + cy.get('@org').then(({id}: Organization) => { + cy.visit(`${orgs}/${id}/tasks`) }) }) - cy.getByTestID('task-save-btn').click() - cy.getByTestID('task-card') - .should('have.length', 2) - .and('contain', firstTask) - .and('contain', secondTask) - cy.getByTestID('task-card--name') - .contains(firstTask) - .click() }) it('when navigating using the navbar', () => { - // verify that the previously input data exists - cy.getByInputValue(firstTask) - // navigate home - cy.get('div.cf-nav--item.active').click() // click on the second task cy.getByTestID('task-card--name') .contains(secondTask) @@ -368,10 +335,6 @@ http.post( }) it('when navigating using the cancel button', () => { - // verify that the previously input data exists - cy.getByInputValue(firstTask) - // navigate home - cy.getByTestID('task-cancel-btn').click() // click on the second task cy.getByTestID('task-card--name') .contains(secondTask) @@ -388,10 +351,6 @@ http.post( }) it('when navigating using the save button', () => { - // verify that the previously input data exists - cy.getByInputValue(firstTask) - // navigate home - cy.getByTestID('task-save-btn').click() // click on the second task cy.getByTestID('task-card--name') .contains(secondTask) @@ -421,16 +380,16 @@ function createFirstTask( cy.getByTestID('add-resource-dropdown--new').click() - cy.getByInputName('name').type(name) - cy.getByTestID('task-form-schedule-input').type(interval) - cy.getByTestID('task-form-offset-input').type(offset) - cy.get('@bucket').then(bucket => { cy.getByTestID('flux-editor').within(() => { - cy.get('.react-monaco-editor-container') + cy.get('textarea.inputarea') .click() .focused() .type(flux(bucket), {force: true, delay: 2}) }) }) + + cy.getByInputName('name').type(name) + cy.getByTestID('task-form-schedule-input').type(interval) + cy.getByTestID('task-form-offset-input').type(offset) } diff --git a/ui/global.d.ts b/ui/global.d.ts index 95dac61261..f4389d8aa2 100644 --- a/ui/global.d.ts +++ b/ui/global.d.ts @@ -1,5 +1,16 @@ +import {MonacoType} from 'src/types' + // // got some globals here that only exist during compilation // declare module '*.png' +declare let monaco: MonacoType + +declare global { + interface Window { + monaco: MonacoType + } +} + +window.monaco = window.monaco || {} diff --git a/ui/package.json b/ui/package.json index 1002583b1d..bbc3536105 100644 --- a/ui/package.json +++ b/ui/package.json @@ -130,6 +130,7 @@ }, "dependencies": { "@influxdata/clockface": "1.1.5", + "@influxdata/flux-lsp-browser": "^0.2.2", "@influxdata/flux-parser": "^0.3.0", "@influxdata/giraffe": "0.17.4", "@influxdata/influx": "0.5.5", @@ -154,9 +155,10 @@ "lodash": "^4.3.0", "memoize-one": "^4.0.2", "moment": "^2.13.0", - "monaco-editor": "^0.18.1", + "monaco-editor": "^0.19.2", "monaco-editor-textmate": "^2.2.1", - "monaco-editor-webpack-plugin": "^1.7.0", + "monaco-languageclient": "^0.11.0", + "monaco-editor-webpack-plugin": "^1.8.2", "monaco-textmate": "^3.0.1", "normalizr": "^3.4.1", "onigasm": "^2.2.4", @@ -174,7 +176,7 @@ "react-grid-layout": "^0.16.6", "react-loadable": "^5.5.0", "react-markdown": "^4.0.3", - "react-monaco-editor": "^0.32.1", + "react-monaco-editor": "^0.33.0", "react-redux": "^5.1.2", "react-router": "^3.0.2", "react-router-redux": "^4.0.8", diff --git a/ui/src/external/constants.ts b/ui/src/external/constants.ts new file mode 100644 index 0000000000..5a2e26ec3d --- /dev/null +++ b/ui/src/external/constants.ts @@ -0,0 +1 @@ +export const FLUXLANGID = 'flux' as const diff --git a/ui/src/external/monaco.fluxCompletions.ts b/ui/src/external/monaco.fluxCompletions.ts index 27c1ead808..15231e3634 100644 --- a/ui/src/external/monaco.fluxCompletions.ts +++ b/ui/src/external/monaco.fluxCompletions.ts @@ -1,20 +1,83 @@ -// Types -import {MonacoType} from 'src/types' +// Libraries +import {completion, sendMessage} from 'src/external/monaco.lspMessages' +import { + MonacoToProtocolConverter, + ProtocolToMonacoConverter, +} from 'monaco-languageclient/lib/monaco-converter' +import {get} from 'lodash' -export const addSnippets = (monaco: MonacoType) => { - monaco.languages.registerCompletionItemProvider('flux', { - provideCompletionItems: () => { - const suggestions = [ - { - label: 'from', - kind: monaco.languages.CompletionItemKind.Snippet, - insertText: ['from(bucket: ${1})', '\t|>'].join('\n'), - insertTextRules: - monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, - documentation: 'From-Statement', - }, - ] as any[] - return {suggestions: suggestions} - }, - }) +// Constants +import {FLUXLANGID} from 'src/external/constants' + +// Types +import {CompletionItem} from 'monaco-languageclient/lib/services' +import {MonacoType} from 'src/types' +import {IDisposable} from 'monaco-editor' + +const m2p = new MonacoToProtocolConverter() +const p2m = new ProtocolToMonacoConverter() + +export const registerCompletion = (monaco: MonacoType, server): IDisposable => { + const completionProvider = monaco.languages.registerCompletionItemProvider( + FLUXLANGID, + { + provideCompletionItems: (model, position, context) => { + const wordUntil = model.getWordUntilPosition(position) + const defaultRange = new monaco.Range( + position.lineNumber, + wordUntil.startColumn, + position.lineNumber, + wordUntil.endColumn + ) + const response = sendMessage( + completion( + m2p.asPosition(position.lineNumber, position.column), + context + ), + server + ) + + const completionItems = get( + response, + 'result.items', + null + ) as CompletionItem[] + + if (!completionItems) { + return + } + return p2m.asCompletionResult(completionItems, defaultRange) + }, + triggerCharacters: [ + '.', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + ], + } + ) + return completionProvider } diff --git a/ui/src/external/monaco.fluxLang.ts b/ui/src/external/monaco.fluxLang.ts index 518be2bf00..c922208c56 100644 --- a/ui/src/external/monaco.fluxLang.ts +++ b/ui/src/external/monaco.fluxLang.ts @@ -1,7 +1,10 @@ -export const tokenizeFlux = monaco => { - monaco.languages.register({id: 'flux'}) +// Constants +import {FLUXLANGID} from 'src/external/constants' - monaco.languages.setMonarchTokensProvider('flux', { +export const tokenizeFlux = monaco => { + monaco.languages.register({id: FLUXLANGID}) + + monaco.languages.setMonarchTokensProvider(FLUXLANGID, { keywords: ['from', 'range', 'filter', 'to'], tokenizer: { root: [ diff --git a/ui/src/external/monaco.fluxSyntax.ts b/ui/src/external/monaco.fluxSyntax.ts index 185785ca95..8dc6247602 100644 --- a/ui/src/external/monaco.fluxSyntax.ts +++ b/ui/src/external/monaco.fluxSyntax.ts @@ -2,14 +2,15 @@ import loader from 'src/external/monaco.onigasm' import {Registry} from 'monaco-textmate' // peer dependency import {wireTmGrammars} from 'monaco-editor-textmate' +// Constants +import {FLUXLANGID} from 'src/external/constants' + // Types import {MonacoType} from 'src/types' export async function addSyntax(monaco: MonacoType) { await loader() - monaco.languages.register({id: 'flux'}) - const registry = new Registry({ // TODO: this is maintained in influxdata/vsflux, which is currently // a private repo, so we can't use it yet (alex) @@ -25,7 +26,17 @@ export async function addSyntax(monaco: MonacoType) { // map of monaco "language id's" to TextMate scopeNames const grammars = new Map() - grammars.set('flux', 'flux') + grammars.set(FLUXLANGID, FLUXLANGID) + + monaco.languages.setLanguageConfiguration(FLUXLANGID, { + autoClosingPairs: [ + {open: '"', close: '"'}, + {open: '[', close: ']'}, + {open: "'", close: "'"}, + {open: '{', close: '}'}, + {open: '(', close: ')'}, + ], + }) await wireTmGrammars(monaco, registry, grammars) } diff --git a/ui/src/external/monaco.lspMessages.ts b/ui/src/external/monaco.lspMessages.ts new file mode 100644 index 0000000000..763f496649 --- /dev/null +++ b/ui/src/external/monaco.lspMessages.ts @@ -0,0 +1,94 @@ +import {get} from 'lodash' + +// Constants +import {FLUXLANGID} from 'src/external/constants' + +// Types +import {ServerResponse} from 'src/types' + +type LSPMessage = + | typeof initialize + | ReturnType + | ReturnType + | ReturnType + +const URI = 'monacoeditor' as const +const JSONRPC = '2.0' as const + +export const initialize = { + jsonrpc: JSONRPC, + id: 1, + method: 'initialize', + params: {}, +} as const + +export const didOpen = (text: string) => ({ + jsonrpc: JSONRPC, + id: 2, + method: 'textDocument/didOpen' as const, + params: { + textDocument: { + uri: URI, + languageId: FLUXLANGID, + version: 1 as const, + text, + }, + }, +}) + +export const didChange = ( + newText: string, + version: number, + messageID: number +) => ({ + jsonrpc: JSONRPC, + id: messageID, + method: 'textDocument/didChange' as const, + params: { + textDocument: { + uri: URI, + version: version, + }, + contentChanges: [ + { + text: newText, + }, + ], + }, +}) + +export const completion = (position, context) => ({ + jsonrpc: JSONRPC, + id: 100, + method: 'textDocument/completion' as const, + params: { + textDocument: {uri: URI}, + position, + context, + }, +}) + +export const parseResponse = (response: ServerResponse): null | object => { + const message = response.get_message() + if (message) { + const split = message.split('\n') + const parsed_msg = get(split, '2', null) + return JSON.parse(parsed_msg) + } else { + const error = response.get_error() + const split = error.split('\n') + const parsed_err = get(split, '2', null) + return JSON.parse(parsed_err) + } +} + +export const sendMessage = (message: LSPMessage, server) => { + const stringifiedMessage = JSON.stringify(message) + const size = stringifiedMessage.length + + const resp = server.process( + `Content-Length: ${size}\r\n\r\n` + stringifiedMessage + ) + + return parseResponse(resp) +} diff --git a/ui/src/shared/components/FluxMonacoEditor.tsx b/ui/src/shared/components/FluxMonacoEditor.tsx index d2ee3612ef..f2e2fcf293 100644 --- a/ui/src/shared/components/FluxMonacoEditor.tsx +++ b/ui/src/shared/components/FluxMonacoEditor.tsx @@ -1,58 +1,72 @@ // Libraries -import React, {FC} from 'react' +import React, {FC, useEffect, useRef, useState} from 'react' +import {Server} from '@influxdata/flux-lsp-browser' // Components import MonacoEditor from 'react-monaco-editor' // Utils import addFluxTheme, {THEME_NAME} from 'src/external/monaco.fluxTheme' -import {addSnippets} from 'src/external/monaco.fluxCompletions' +import {registerCompletion} from 'src/external/monaco.fluxCompletions' import {addSyntax} from 'src/external/monaco.fluxSyntax' -import {OnChangeScript} from 'src/types/flux' import {addKeyBindings} from 'src/external/monaco.keyBindings' +import { + sendMessage, + initialize, + didChange, + didOpen, +} from 'src/external/monaco.lspMessages' + +// Constants +import {FLUXLANGID} from 'src/external/constants' // Types +import {OnChangeScript} from 'src/types/flux' import {MonacoType, EditorType} from 'src/types' import './FluxMonacoEditor.scss' -interface Position { - line: number - ch: number -} - interface Props { script: string onChangeScript: OnChangeScript onSubmitScript?: () => void - onCursorChange?: (position: Position) => void + setEditorInstance?: (editor: EditorType) => void } -const FluxEditorMonaco: FC = props => { +const FluxEditorMonaco: FC = ({ + script, + onChangeScript, + onSubmitScript, + setEditorInstance, +}) => { + let completionProvider = {dispose: () => {}} + const lspServer = useRef(new Server(false)) + const [docVersion, setdocVersion] = useState(2) + const [msgID, setmsgID] = useState(3) + + useEffect(() => { + sendMessage(initialize, lspServer.current) + sendMessage(didOpen(script), lspServer.current) + return () => { + completionProvider.dispose() + } + }, []) + const editorWillMount = (monaco: MonacoType) => { + monaco.languages.register({id: FLUXLANGID}) addFluxTheme(monaco) - addSnippets(monaco) addSyntax(monaco) + completionProvider = registerCompletion(monaco, lspServer.current) } const editorDidMount = (editor: EditorType, monaco: MonacoType) => { + if (setEditorInstance) { + setEditorInstance(editor) + } addKeyBindings(editor, monaco) - editor.onDidChangeCursorPosition(evt => { - const {position} = evt - const {onCursorChange} = props - const pos = { - line: position.lineNumber - 1, - ch: position.column, - } - - if (onCursorChange) { - onCursorChange(pos) - } - }) - + editor.focus() editor.onKeyUp(evt => { const {ctrlKey, code} = evt - const {onSubmitScript} = props if (ctrlKey && code === 'Enter') { if (onSubmitScript) { onSubmitScript() @@ -60,15 +74,21 @@ const FluxEditorMonaco: FC = props => { } }) } - const {script, onChangeScript} = props + + const onChange = (text: string) => { + sendMessage(didChange(text, docVersion, msgID), lspServer.current) + setdocVersion(docVersion + 1) + setmsgID(msgID + 1) + onChangeScript(text) + } return (
{ - private cursorPosition: Position = {line: 0, ch: 0} +const TimeMachineFluxEditor: FC = ({ + activeQueryText, + onSubmitQueries, + onSetActiveQueryText, + activeTab, +}) => { + const [displayFluxFunctions, setDisplayFluxFunctions] = useState(true) + const [editorInstance, setEditorInstance] = useState(null) - public state: State = { - displayFluxFunctions: true, + const showFluxFunctions = () => { + setDisplayFluxFunctions(true) } - public render() { - const { - activeQueryText, - onSubmitQueries, - onSetActiveQueryText, - activeTab, - } = this.props + const hideFluxFunctions = () => { + setDisplayFluxFunctions(false) + } - const divisions = [ + const handleInsertVariable = (variableName: string): void => { + const p = editorInstance.getPosition() + editorInstance.executeEdits('', [ { - size: 0.75, - handleDisplay: HANDLE_NONE, - render: () => { - return ( - - ) - }, + range: new window.monaco.Range( + p.lineNumber, + p.column, + p.lineNumber, + p.column + ), + text: `v.${variableName}`, }, + ]) + onSetActiveQueryText(editorInstance.getValue()) + } + + const handleInsertFluxFunction = (func: FluxToolbarFunction): void => { + const p = editorInstance.getPosition() + const edits = [ { - render: () => { - return ( - <> -
- {activeTab !== 'customCheckQuery' && ( - - )} - -
- {this.rightDivision} - - ) - }, - handlePixels: 6, - size: 0.25, + range: new window.monaco.Range( + p.lineNumber, + p.column, + p.lineNumber, + p.column + ), + text: formatFunctionForInsert(func.name, func.example), }, ] - - return ( -
- -
+ const importStatement = generateImport( + func.package, + editorInstance.getValue() ) - } - - private get rightDivision(): JSX.Element { - const {displayFluxFunctions} = this.state - - if (displayFluxFunctions) { - return ( - - ) + if (importStatement) { + edits.unshift({ + range: new window.monaco.Range(1, 1, 1, 1), + text: `${importStatement}\n`, + }) } - - return + editorInstance.executeEdits('', edits) + onSetActiveQueryText(editorInstance.getValue()) } - private handleCursorPosition = (position: Position): void => { - this.cursorPosition = position - } + const divisions = [ + { + size: 0.75, + handleDisplay: HANDLE_NONE, + render: () => { + return ( + + ) + }, + }, + { + render: () => { + return ( + <> +
+ {activeTab !== 'customCheckQuery' && ( + + )} + +
+ {displayFluxFunctions ? ( + + ) : ( + + )} + + ) + }, + handlePixels: 6, + size: 0.25, + }, + ] - private handleInsertVariable = (variableName: string): void => { - const {activeQueryText} = this.props - const {line, ch} = this.cursorPosition - - const {updatedScript, cursorPosition} = insertVariable( - line, - ch, - activeQueryText, - variableName - ) - - this.props.onSetActiveQueryText(updatedScript) - - this.handleCursorPosition(cursorPosition) - } - - private handleInsertFluxFunction = (func: FluxToolbarFunction): void => { - const {activeQueryText, onSetActiveQueryText} = this.props - const {line} = this.cursorPosition - - const {updatedScript, cursorPosition} = insertFluxFunction( - line, - activeQueryText, - func - ) - - onSetActiveQueryText(updatedScript) - this.handleCursorPosition(cursorPosition) - } - - private showFluxFunctions = () => { - this.setState({displayFluxFunctions: true}) - } - - private hideFluxFunctions = () => { - this.setState({displayFluxFunctions: false}) - } + return ( +
+ +
+ ) } const mstp = (state: AppState) => { diff --git a/ui/src/timeMachine/utils/insertFunction.ts b/ui/src/timeMachine/utils/insertFunction.ts index 315142a1fa..3b8fcfe2a2 100644 --- a/ui/src/timeMachine/utils/insertFunction.ts +++ b/ui/src/timeMachine/utils/insertFunction.ts @@ -1,44 +1,6 @@ -import {Position} from 'codemirror' - // Constants import {FROM, UNION} from 'src/shared/constants/fluxFunctions' -// Types -import {FluxToolbarFunction} from 'src/types' - -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: @@ -50,7 +12,10 @@ const functionRequiresNewLine = (funcName: string): boolean => { } } -const formatFunctionForInsert = (funcName: string, fluxFunction: string) => { +export const formatFunctionForInsert = ( + funcName: string, + fluxFunction: string +) => { if (functionRequiresNewLine(funcName)) { return `\n${fluxFunction}` } @@ -58,63 +23,15 @@ const formatFunctionForInsert = (funcName: string, fluxFunction: string) => { return ` |> ${fluxFunction}` } -const getCursorPosition = ( - insertLineNumber, - formattedFunction, - funcName -): Position => { - const ch = formattedFunction.length - 1 - const line = functionRequiresNewLine(funcName) - ? insertLineNumber + 1 - : insertLineNumber - - return {line, ch} -} - -const genImport = (script: string, funcPackage: string) => { +export const generateImport = ( + funcPackage: string, + script: string +): false | string => { const importStatement = `import "${funcPackage}"` if (!funcPackage || script.includes(importStatement)) { - return '' + return false } return importStatement } - -export const insertFluxFunction = ( - currentLineNumber: number, - currentScript: string, - func: FluxToolbarFunction -): {updatedScript: string; cursorPosition: Position} => { - const {name, example} = func - - const scriptLines = currentScript.split('\n') - - let insertLineNumber = getInsertLineNumber(currentLineNumber, scriptLines) - - const insertOnSameLine = currentLineNumber === insertLineNumber - - const formattedFunction = formatFunctionForInsert(name, example) - - let nextScript = insertAtLine( - insertLineNumber, - scriptLines, - formattedFunction, - insertOnSameLine - ) - - const importStatement = genImport(nextScript, func.package) - - if (importStatement) { - nextScript = `${importStatement}\n${nextScript}` - insertLineNumber += 1 - } - - const nextCursorPos = getCursorPosition( - insertLineNumber, - formattedFunction, - name - ) - - return {updatedScript: nextScript, cursorPosition: nextCursorPos} -} diff --git a/ui/src/timeMachine/utils/insertVariable.ts b/ui/src/timeMachine/utils/insertVariable.ts deleted file mode 100644 index 44d6fa0c88..0000000000 --- a/ui/src/timeMachine/utils/insertVariable.ts +++ /dev/null @@ -1,60 +0,0 @@ -import {Position} from 'codemirror' - -const rejoinScript = (scriptLines: string[]): string => { - return scriptLines.join('\n') -} - -const getCursorPosition = ( - currentLineNumber: number, - currentCharacterNumber: number, - variableName: string -) => { - return { - line: currentLineNumber, - ch: currentCharacterNumber + formatVariable(variableName).length, - } -} - -const insertAtCharacter = ( - lineNumber: number, - characterNumber: number, - scriptLines: string[], - variableName: string -): string => { - const lineToEdit = scriptLines[lineNumber] - const front = lineToEdit.slice(0, characterNumber) - const back = lineToEdit.slice(characterNumber, lineToEdit.length) - - const updatedLine = `${front}${formatVariable(variableName)}${back}` - scriptLines[lineNumber] = updatedLine - - return rejoinScript(scriptLines) -} - -const formatVariable = (variableName: string): string => { - return `v.${variableName}` -} - -export const insertVariable = ( - currentLineNumber: number, - currentCharacterNumber: number, - currentScript: string, - variableName: string -): {updatedScript: string; cursorPosition: Position} => { - const scriptLines = currentScript.split('\n') - - const updatedScript = insertAtCharacter( - currentLineNumber, - currentCharacterNumber, - scriptLines, - variableName - ) - - const updatedCursorPosition = getCursorPosition( - currentLineNumber, - currentCharacterNumber, - variableName - ) - - return {updatedScript, cursorPosition: updatedCursorPosition} -} diff --git a/ui/src/types/monaco.ts b/ui/src/types/monaco.ts index 4e6085a65c..3b9eb0fc1b 100644 --- a/ui/src/types/monaco.ts +++ b/ui/src/types/monaco.ts @@ -1,4 +1,7 @@ import * as allMonaco from 'monaco-editor/esm/vs/editor/editor.api' +import * as lsp from '@influxdata/flux-lsp-browser' + +export type ServerResponse = lsp.ServerResponse export type MonacoType = typeof allMonaco export type EditorType = allMonaco.editor.IStandaloneCodeEditor diff --git a/ui/tsconfig.json b/ui/tsconfig.json index 6635aeca43..3d4392f09b 100644 --- a/ui/tsconfig.json +++ b/ui/tsconfig.json @@ -40,5 +40,6 @@ "src/**/*.test.tsx", "src/**/mocks.ts", "coverage" - ] + ], + "include": ["global.d.ts"] } diff --git a/ui/webpack.common.ts b/ui/webpack.common.ts index bd251740ab..fc3f44a6d2 100644 --- a/ui/webpack.common.ts +++ b/ui/webpack.common.ts @@ -2,6 +2,7 @@ const path = require('path') const HtmlWebpackPlugin = require('html-webpack-plugin') const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin') const {CleanWebpackPlugin} = require('clean-webpack-plugin') const webpack = require('webpack') const { @@ -29,14 +30,29 @@ module.exports = { }, extensions: ['.tsx', '.ts', '.js', '.wasm'], }, + node: { + fs: 'empty', + global: true, + crypto: 'empty', + tls: 'empty', + net: 'empty', + process: true, + module: false, + clearImmediate: false, + setImmediate: true, + }, module: { rules: [ { - test: /\flux_parser_bg.wasm$/, + test: /flux_parser_bg.wasm$/, type: 'webassembly/experimental', }, { - test: /^((?!flux_parser_bg).)*.wasm$/, + test: /flux-lsp-browser_bg.wasm$/, + type: 'webassembly/experimental', + }, + { + test: /^((?!flux_parser_bg|flux-lsp-browser_bg).)*.wasm$/, loader: 'file-loader', type: 'javascript/auto', }, @@ -120,6 +136,10 @@ module.exports = { API_PREFIX: API_BASE_PATH, STATIC_PREFIX: BASE_PATH, }), + new MonacoWebpackPlugin({ + languages: ['json', 'markdown'], + features: ['!gotoSymbol'], + }), ], stats: { colors: true, diff --git a/ui/webpack.vendor.ts b/ui/webpack.vendor.ts index 52bd0b41fd..d9be6f740a 100644 --- a/ui/webpack.vendor.ts +++ b/ui/webpack.vendor.ts @@ -20,6 +20,24 @@ module.exports = { entry: { vendor, }, + resolve: { + alias: { + vscode: path.resolve( + './node_modules/monaco-languageclient/lib/vscode-compatibility' + ), + }, + }, + node: { + fs: 'empty', + global: true, + crypto: 'empty', + tls: 'empty', + net: 'empty', + process: true, + module: false, + clearImmediate: false, + setImmediate: true, + }, output: { path: path.join(__dirname, 'build'), filename: '[name].bundle.js', @@ -32,6 +50,14 @@ module.exports = { include: MONACO_DIR, use: ['style-loader', 'css-loader'], }, + { + test: /\.(woff|woff2|eot|ttf|otf)$/, + use: [ + { + loader: 'file-loader', + }, + ], + }, ], }, plugins: [ @@ -40,7 +66,7 @@ module.exports = { path: path.join(__dirname, 'build', '[name]-manifest.json'), }), new MonacoWebpackPlugin({ - languages: ['json', 'javascript', 'go', 'markdown'], + languages: ['json', 'markdown'], }), ], stats: { diff --git a/ui/yarn.lock b/ui/yarn.lock index ffbfcaba48..0f5968932b 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1016,6 +1016,11 @@ resolved "https://registry.yarnpkg.com/@influxdata/clockface/-/clockface-1.1.5.tgz#dfedc4f59788717d5e92bd00935dda35c0c60fc8" integrity sha512-5+RDswLCiOBX21rey6e1j4vrwQ6obwMv+qcP6ucH7y0vTRnAaMk9lqKqHH3FGUqUEfBNegtj4Ho8430ywfS6ag== +"@influxdata/flux-lsp-browser@^0.2.2": + version "0.2.2" + resolved "https://registry.yarnpkg.com/@influxdata/flux-lsp-browser/-/flux-lsp-browser-0.2.2.tgz#766ef965da25149ac300df6db1a64ad277b5b50b" + integrity sha512-MlFmu7gVdgJ1pkoC1CRaWmjedi6IuSJa2Bg3vDl0l/1cJyAShtoDs8levYvL0gqcKyvq/i/baXrNXF+SCIM7MQ== + "@influxdata/flux-parser@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@influxdata/flux-parser/-/flux-parser-0.3.0.tgz#b63123ac814ad32c65e46a4097ba3d8b959416a5" @@ -1486,11 +1491,6 @@ resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.2.tgz#a811b8c18e2babab7d542b3365887ae2e4d9de47" integrity sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg== -"@types/source-list-map@*": - version "0.1.2" - resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9" - integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA== - "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -1520,27 +1520,6 @@ dependencies: "@types/node" "*" -"@types/webpack-sources@*": - version "0.1.5" - resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-0.1.5.tgz#be47c10f783d3d6efe1471ff7f042611bd464a92" - integrity sha512-zfvjpp7jiafSmrzJ2/i3LqOyTYTuJ7u1KOXlKgDlvsj9Rr0x7ZiYu5lZbXwobL7lmsRNtPXlBfmaUD8eU2Hu8w== - dependencies: - "@types/node" "*" - "@types/source-list-map" "*" - source-map "^0.6.1" - -"@types/webpack@^4.4.19": - version "4.39.8" - resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.39.8.tgz#8083a4eb850ea02961ef6161465434c9b478851f" - integrity sha512-lkJvwNJQUPW2SbVwAZW9s9whJp02nzLf2yTNwMULa4LloED9MYS1aNnGeoBCifpAI1pEBkTpLhuyRmBnLEOZAA== - dependencies: - "@types/anymatch" "*" - "@types/node" "*" - "@types/tapable" "*" - "@types/uglify-js" "*" - "@types/webpack-sources" "*" - source-map "^0.6.0" - "@types/webpack@^4.4.31", "@types/webpack@^4.4.35": version "4.4.35" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.35.tgz#b7088eb2d471d5645e5503d272783cafa753583b" @@ -5380,6 +5359,11 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" +glob-to-regexp@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz#8c5a1494d2066c570cc3bfe4496175acc4d502ab" + integrity sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs= + glob@7.1.2: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" @@ -7908,17 +7892,27 @@ monaco-editor-textmate@^2.2.1: resolved "https://registry.yarnpkg.com/monaco-editor-textmate/-/monaco-editor-textmate-2.2.1.tgz#93f3f1932061dd2311b92a42ea1c027cfeb3e439" integrity sha512-RYTNNfvyjK15M0JA8WIi9UduU10eX5724UGNKnaA8MSetehjThGENctUTuKaxPk/k3pq59QzaQ/C06A44iJd3Q== -monaco-editor-webpack-plugin@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz#920cbeecca25f15d70d568a7e11b0ba4daf1ae83" - integrity sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA== +monaco-editor-webpack-plugin@^1.8.2: + version "1.8.2" + resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.8.2.tgz#3721b8d9a3e2e41b154cf2a2955a7d7246c03714" + integrity sha512-g9G7A/lxQtpPsYaZFBqm73dwVkOziGUXExIR6iW7ksZUaiMkpvdTiE9O8edgdJGo+XtCmjycmIKB1Lt8VKbSTQ== dependencies: - "@types/webpack" "^4.4.19" + loader-utils "^1.2.3" -monaco-editor@^0.18.1: - version "0.18.1" - resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.18.1.tgz#ced7c305a23109875feeaf395a504b91f6358cfc" - integrity sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw== +monaco-editor@^0.19.2: + version "0.19.3" + resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.19.3.tgz#1c994b3186c00650dbcd034d5370d46bf56c0663" + integrity sha512-2n1vJBVQF2Hhi7+r1mMeYsmlf18hjVb6E0v5SoMZyb4aeOmYPKun+CE3gYpiNA1KEvtSdaDHFBqH9d7Wd9vREg== + +monaco-languageclient@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/monaco-languageclient/-/monaco-languageclient-0.11.0.tgz#0968ec143c98bf2c9fa69c2a84ac9ad5448e039d" + integrity sha512-aqvgI+gX5K711eCJ3ggsMIeJ5fv7LnhxgzkMPRxSrkp+5/KLKY80V/m7PzDvWri+zF5cZ6InIPSmAAekn4oRzA== + dependencies: + glob-to-regexp "^0.3.0" + vscode-jsonrpc "^4.1.0-next" + vscode-languageclient "^5.3.0-next" + vscode-uri "^1.0.5" monaco-textmate@^3.0.1: version "3.0.1" @@ -9410,7 +9404,7 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.6, prop-types@^15.5.8, pr loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.0.0, prop-types@^15.5.0, prop-types@^15.7.2: +prop-types@^15.5.0, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -9772,13 +9766,13 @@ react-markdown@^4.0.3: unist-util-visit "^1.3.0" xtend "^4.0.1" -react-monaco-editor@^0.32.1: - version "0.32.1" - resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.32.1.tgz#fa45d62fd19d5942cba98bd7c59336d21f8750e0" - integrity sha512-gJjU9Rx/QuJr+Y4C0MSidMdkh1hmHGneIU8yI87bc5kd46ZXPNETqiigyUB7pKy4ZSuFHBhjhg2lgESaID43ag== +react-monaco-editor@^0.33.0: + version "0.33.0" + resolved "https://registry.yarnpkg.com/react-monaco-editor/-/react-monaco-editor-0.33.0.tgz#822c331836ec39b1160faf22b0c722266f822460" + integrity sha512-zFo3A55QHCABYm4Xt0HWQMq10GI+oxhhCUGDYgzLksU1iGrdvHudUNXTDZvE43B1gM+cPyJ75jla/464kss8Iw== dependencies: "@types/react" "^15.x || ^16.x" - prop-types "^15.0.0" + prop-types "^15.7.2" react-onclickoutside@^6.7.1: version "6.7.1" @@ -12028,6 +12022,42 @@ vm-browserify@^1.0.1: resolved "https://registry.yarnpkg.com/vm-browserify/-/vm-browserify-1.1.2.tgz#78641c488b8e6ca91a75f511e7a3b32a86e5dda0" integrity sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ== +vscode-jsonrpc@^4.1.0-next: + version "4.1.0-next.3" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-4.1.0-next.3.tgz#05fe742959a2726020d4d0bfbc3d3c97873c7fde" + integrity sha512-Z6oxBiMks2+UADV1QHXVooSakjyhI+eHTnXzDyVvVMmegvSfkXk2w6mPEdSkaNHFBdtWW7n20H1yw2nA3A17mg== + +vscode-jsonrpc@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794" + integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A== + +vscode-languageclient@^5.3.0-next: + version "5.3.0-next.9" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-5.3.0-next.9.tgz#34f58017647f15cd86015f7af45935dc750611f7" + integrity sha512-BFA3X1y2EI2CfsSBy0KG2Xr5BOYfd/97jTmD+doqL6oj+cY8S7AmRCOwb2f9Hbjq8GWL7YC+OJ0leZEUSPgP0A== + dependencies: + semver "^6.3.0" + vscode-languageserver-protocol "^3.15.0-next.8" + +vscode-languageserver-protocol@^3.15.0-next.8: + version "3.15.2" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.2.tgz#e52c62923140b2655ad2472f6f29cfb83bacf5b8" + integrity sha512-GdL05JKOgZ76RDg3suiGCl9enESM7iQgGw4x93ibTh4sldvZmakHmTeZ4iUApPPGKf6O3OVBtrsksBXnHYaxNg== + dependencies: + vscode-jsonrpc "^5.0.1" + vscode-languageserver-types "3.15.1" + +vscode-languageserver-types@3.15.1: + version "3.15.1" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de" + integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ== + +vscode-uri@^1.0.5: + version "1.0.8" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-1.0.8.tgz#9769aaececae4026fb6e22359cb38946580ded59" + integrity sha512-obtSWTlbJ+a+TFRYGaUumtVwb+InIUVI0Lu0VBUAPmj2cU5JutEXg3xUE0c2J5Tcy7h2DEKVJBFi+Y9ZSFzzPQ== + w3c-hr-time@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"