From 503dccb771ffa5cdf22ecdb9fccef5307ea372d8 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 17 Sep 2019 13:21:50 +0200 Subject: [PATCH] Revert "Chore: Update Slate to 0.47.8 (#18412)" (#19167) This reverts commit 601853fc8453f73fb5df4bd0fe8c2a228aed2d3d. --- package.json | 11 +- .../src/config/webpack.plugin.config.ts | 2 +- packages/grafana-ui/package.json | 5 - packages/grafana-ui/rollup.config.ts | 7 +- .../components/DataLinks/DataLinkInput.tsx | 46 +- packages/grafana-ui/src/index.ts | 1 - .../grafana-ui/src/slate-plugins/index.ts | 1 - .../slate-plugins/slate-prism/TOKEN_MARK.ts | 3 - .../src/slate-plugins/slate-prism/index.ts | 160 ----- .../src/slate-plugins/slate-prism/options.tsx | 77 --- packages/grafana-ui/src/utils/slate.ts | 27 +- packages/grafana-ui/tsconfig.json | 4 - .../app/features/explore/QueryField.test.tsx | 41 ++ public/app/features/explore/QueryField.tsx | 607 ++++++++++++++++-- public/app/features/explore/Typeahead.tsx | 224 +++---- public/app/features/explore/TypeaheadInfo.tsx | 25 +- public/app/features/explore/TypeaheadItem.tsx | 36 +- .../explore/slate-plugins/braces.test.ts | 39 ++ .../explore/slate-plugins/braces.test.tsx | 40 -- .../features/explore/slate-plugins/braces.ts | 40 +- .../explore/slate-plugins/clear.test.ts | 39 ++ .../explore/slate-plugins/clear.test.tsx | 42 -- .../features/explore/slate-plugins/clear.ts | 21 +- .../explore/slate-plugins/clipboard.ts | 61 -- .../explore/slate-plugins/indentation.ts | 93 --- .../features/explore/slate-plugins/newline.ts | 21 +- .../explore/slate-plugins/runner.test.tsx | 17 - .../features/explore/slate-plugins/runner.ts | 7 +- .../slate-plugins/selection_shortcuts.ts | 72 --- .../explore/slate-plugins/suggestions.tsx | 313 --------- .../app/features/explore/utils/typeahead.ts | 8 +- public/app/features/plugins/plugin_loader.ts | 4 +- .../components/ElasticsearchQueryField.tsx | 8 +- .../editor/KustoQueryField.tsx | 17 +- .../editor/query_field.tsx | 59 +- .../loki/components/LokiQueryField.tsx | 3 +- .../loki/components/LokiQueryFieldForm.tsx | 49 +- .../loki/components/useLokiSyntax.test.ts | 1 - .../loki/components/useLokiSyntax.ts | 1 + .../datasource/loki/language_provider.test.ts | 57 +- .../datasource/loki/language_provider.ts | 54 +- public/app/plugins/datasource/loki/mocks.ts | 4 +- public/app/plugins/datasource/loki/syntax.ts | 4 +- .../prometheus/components/PromQueryField.tsx | 34 +- .../prometheus/language_provider.ts | 145 ++--- .../datasource/prometheus/language_utils.ts | 8 +- .../specs/language_provider.test.ts | 222 +++---- public/app/types/explore.ts | 32 +- public/sass/components/_slate_editor.scss | 8 +- tsconfig.json | 3 +- yarn.lock | 220 ++++--- 51 files changed, 1318 insertions(+), 1705 deletions(-) delete mode 100644 packages/grafana-ui/src/slate-plugins/index.ts delete mode 100644 packages/grafana-ui/src/slate-plugins/slate-prism/TOKEN_MARK.ts delete mode 100644 packages/grafana-ui/src/slate-plugins/slate-prism/index.ts delete mode 100644 packages/grafana-ui/src/slate-plugins/slate-prism/options.tsx create mode 100644 public/app/features/explore/slate-plugins/braces.test.ts delete mode 100644 public/app/features/explore/slate-plugins/braces.test.tsx create mode 100644 public/app/features/explore/slate-plugins/clear.test.ts delete mode 100644 public/app/features/explore/slate-plugins/clear.test.tsx delete mode 100644 public/app/features/explore/slate-plugins/clipboard.ts delete mode 100644 public/app/features/explore/slate-plugins/indentation.ts delete mode 100644 public/app/features/explore/slate-plugins/runner.test.tsx delete mode 100644 public/app/features/explore/slate-plugins/selection_shortcuts.ts delete mode 100644 public/app/features/explore/slate-plugins/suggestions.tsx diff --git a/package.json b/package.json index 0f9a5d882f1..a53ab845d1e 100644 --- a/package.json +++ b/package.json @@ -52,9 +52,7 @@ "@types/redux-logger": "3.0.7", "@types/redux-mock-store": "1.0.1", "@types/reselect": "2.2.0", - "@types/slate": "0.47.1", - "@types/slate-plain-serializer": "0.6.1", - "@types/slate-react": "0.22.5", + "@types/slate": "0.44.11", "@types/tinycolor2": "1.4.2", "angular-mocks": "1.6.6", "autoprefixer": "9.5.0", @@ -195,7 +193,6 @@ }, "dependencies": { "@babel/polyfill": "7.2.5", - "@grafana/slate-react": "0.22.9-grafana", "@torkelo/react-select": "2.4.1", "angular": "1.6.6", "angular-bindonce": "0.3.1", @@ -246,8 +243,10 @@ "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "6.4.0", "search-query-parser": "1.5.2", - "slate": "0.47.8", - "slate-plain-serializer": "0.7.10", + "slate": "0.33.8", + "slate-plain-serializer": "0.5.41", + "slate-prism": "0.5.0", + "slate-react": "0.12.11", "tether": "1.4.5", "tether-drop": "https://github.com/torkelo/drop/tarball/master", "tinycolor2": "1.4.1", diff --git a/packages/grafana-toolkit/src/config/webpack.plugin.config.ts b/packages/grafana-toolkit/src/config/webpack.plugin.config.ts index 34c663c4fe8..07b9f350cb8 100644 --- a/packages/grafana-toolkit/src/config/webpack.plugin.config.ts +++ b/packages/grafana-toolkit/src/config/webpack.plugin.config.ts @@ -149,7 +149,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => { 'emotion', 'prismjs', 'slate-plain-serializer', - '@grafana/slate-react', + 'slate-react', 'react', 'react-dom', 'react-redux', diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index ac98227b8ed..6fdc543fdb4 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -26,12 +26,10 @@ }, "dependencies": { "@grafana/data": "^6.4.0-alpha", - "@grafana/slate-react": "0.22.9-grafana", "@torkelo/react-select": "2.1.1", "@types/react-color": "2.17.0", "classnames": "2.2.6", "d3": "5.9.1", - "immutable": "3.8.2", "jquery": "3.4.1", "lodash": "4.17.15", "moment": "2.24.0", @@ -47,7 +45,6 @@ "react-storybook-addon-props-combinations": "1.1.0", "react-transition-group": "2.6.1", "react-virtualized": "9.21.0", - "slate": "0.47.8", "tinycolor2": "1.4.1" }, "devDependencies": { @@ -68,8 +65,6 @@ "@types/react-custom-scrollbars": "4.0.5", "@types/react-test-renderer": "16.8.1", "@types/react-transition-group": "2.0.16", - "@types/slate": "0.47.1", - "@types/slate-react": "0.22.5", "@types/storybook__addon-actions": "3.4.2", "@types/storybook__addon-info": "4.1.1", "@types/storybook__addon-knobs": "4.0.4", diff --git a/packages/grafana-ui/rollup.config.ts b/packages/grafana-ui/rollup.config.ts index c79a2084fdd..85564fa54e0 100644 --- a/packages/grafana-ui/rollup.config.ts +++ b/packages/grafana-ui/rollup.config.ts @@ -1,6 +1,6 @@ import resolve from 'rollup-plugin-node-resolve'; import commonjs from 'rollup-plugin-commonjs'; -// import sourceMaps from 'rollup-plugin-sourcemaps'; +import sourceMaps from 'rollup-plugin-sourcemaps'; import { terser } from 'rollup-plugin-terser'; const pkg = require('./package.json'); @@ -47,20 +47,19 @@ const buildCjsPackage = ({ env }) => { ], '../../node_modules/react-color/lib/components/common': ['Saturation', 'Hue', 'Alpha'], '../../node_modules/immutable/dist/immutable.js': [ - 'Record', 'Set', 'Map', 'List', 'OrderedSet', 'is', 'Stack', + 'Record', ], - 'node_modules/immutable/dist/immutable.js': ['Record', 'Set', 'Map', 'List', 'OrderedSet', 'is', 'Stack'], '../../node_modules/esrever/esrever.js': ['reverse'], }, }), resolve(), - // sourceMaps(), + sourceMaps(), env === 'production' && terser(), ], }; diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx index 5b57c51aa75..a9c15f4be7b 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx @@ -1,16 +1,19 @@ import React, { useState, useMemo, useCallback, useContext } from 'react'; import { VariableSuggestion, VariableOrigin, DataLinkSuggestions } from './DataLinkSuggestions'; -import { makeValue, ThemeContext, DataLinkBuiltInVars, SCHEMA } from '../../index'; +import { makeValue, ThemeContext, DataLinkBuiltInVars } from '../../index'; import { SelectionReference } from './SelectionReference'; import { Portal } from '../index'; -import { Editor } from '@grafana/slate-react'; -import { Value, Editor as CoreEditor } from 'slate'; +// @ts-ignore +import { Editor } from 'slate-react'; +// @ts-ignore +import { Value, Change, Document } from 'slate'; +// @ts-ignore import Plain from 'slate-plain-serializer'; import { Popper as ReactPopper } from 'react-popper'; import useDebounce from 'react-use/lib/useDebounce'; import { css, cx } from 'emotion'; - -import { SlatePrism } from '../../slate-plugins'; +// @ts-ignore +import PluginPrism from 'slate-prism'; interface DataLinkInputProps { value: string; @@ -19,7 +22,7 @@ interface DataLinkInputProps { } const plugins = [ - SlatePrism({ + PluginPrism({ onlyIn: (node: any) => node.type === 'code_block', getSyntax: () => 'links', }), @@ -76,28 +79,27 @@ export const DataLinkInput: React.FC = ({ value, onChange, s useDebounce(updateUsedSuggestions, 250, [linkUrl]); - const onKeyDown = (event: Event, editor: CoreEditor, next: Function) => { - const keyboardEvent = event as KeyboardEvent; - if (keyboardEvent.key === 'Backspace') { + const onKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Backspace' || event.key === 'Escape') { setShowingSuggestions(false); setSuggestionsIndex(0); } - if (keyboardEvent.key === 'Enter') { + if (event.key === 'Enter') { if (showingSuggestions) { onVariableSelect(currentSuggestions[suggestionsIndex]); } } if (showingSuggestions) { - if (keyboardEvent.key === 'ArrowDown') { - keyboardEvent.preventDefault(); + if (event.key === 'ArrowDown') { + event.preventDefault(); setSuggestionsIndex(index => { return (index + 1) % currentSuggestions.length; }); } - if (keyboardEvent.key === 'ArrowUp') { - keyboardEvent.preventDefault(); + if (event.key === 'ArrowUp') { + event.preventDefault(); setSuggestionsIndex(index => { const nextIndex = index - 1 < 0 ? currentSuggestions.length - 1 : (index - 1) % currentSuggestions.length; return nextIndex; @@ -105,24 +107,21 @@ export const DataLinkInput: React.FC = ({ value, onChange, s } } - if ( - keyboardEvent.key === '?' || - keyboardEvent.key === '&' || - keyboardEvent.key === '$' || - (keyboardEvent.keyCode === 32 && keyboardEvent.ctrlKey) - ) { + if (event.key === '?' || event.key === '&' || event.key === '$' || (event.keyCode === 32 && event.ctrlKey)) { setShowingSuggestions(true); } - if (keyboardEvent.key === 'Backspace') { - return next(); + if (event.key === 'Enter' && showingSuggestions) { + // Preventing entering a new line + // As of https://github.com/ianstormtaylor/slate/issues/1345#issuecomment-340508289 + return false; } else { // @ts-ignore return; } }; - const onUrlChange = ({ value }: { value: Value }) => { + const onUrlChange = ({ value }: Change) => { setLinkUrl(value); }; @@ -187,7 +186,6 @@ export const DataLinkInput: React.FC = ({ value, onChange, s )} { - if (!opts.onlyIn(node)) { - return next(); - } - return decorateNode(opts, Block.create(node as Block)); - }, - - renderDecoration: (props, editor, next) => - opts.renderDecoration( - { - children: props.children, - decoration: props.decoration, - }, - editor as any, - next - ), - }; -} - -/** - * Returns the decoration for a node - */ -function decorateNode(opts: Options, block: Block) { - const grammarName = opts.getSyntax(block); - const grammar = Prism.languages[grammarName]; - if (!grammar) { - // Grammar not loaded - return []; - } - - // Tokenize the whole block text - const texts = block.getTexts(); - const blockText = texts.map(text => text && text.getText()).join('\n'); - const tokens = Prism.tokenize(blockText, grammar); - - // The list of decorations to return - const decorations: Decoration[] = []; - let textStart = 0; - let textEnd = 0; - - texts.forEach(text => { - textEnd = textStart + text!.getText().length; - - let offset = 0; - function processToken(token: string | Prism.Token, accu?: string | number) { - if (typeof token === 'string') { - if (accu) { - const decoration = createDecoration({ - text: text!, - textStart, - textEnd, - start: offset, - end: offset + token.length, - className: `prism-token token ${accu}`, - block, - }); - if (decoration) { - decorations.push(decoration); - } - } - offset += token.length; - } else { - accu = `${accu} ${token.type} ${token.alias || ''}`; - - if (typeof token.content === 'string') { - const decoration = createDecoration({ - text: text!, - textStart, - textEnd, - start: offset, - end: offset + token.content.length, - className: `prism-token token ${accu}`, - block, - }); - if (decoration) { - decorations.push(decoration); - } - - offset += token.content.length; - } else { - // When using token.content instead of token.matchedStr, token can be deep - for (let i = 0; i < token.content.length; i += 1) { - // @ts-ignore - processToken(token.content[i], accu); - } - } - } - } - - tokens.forEach(processToken); - textStart = textEnd + 1; // account for added `\n` - }); - - return decorations; -} - -/** - * Return a decoration range for the given text. - */ -function createDecoration({ - text, - textStart, - textEnd, - start, - end, - className, - block, -}: { - text: Text; // The text being decorated - textStart: number; // Its start position in the whole text - textEnd: number; // Its end position in the whole text - start: number; // The position in the whole text where the token starts - end: number; // The position in the whole text where the token ends - className: string; // The prism token classname - block: Block; -}): Decoration | null { - if (start >= textEnd || end <= textStart) { - // Ignore, the token is not in the text - return null; - } - - // Shrink to this text boundaries - start = Math.max(start, textStart); - end = Math.min(end, textEnd); - - // Now shift offsets to be relative to this text - start -= textStart; - end -= textStart; - - const myDec = block.createDecoration({ - object: 'decoration', - anchor: { - key: text.key, - offset: start, - object: 'point', - }, - focus: { - key: text.key, - offset: end, - object: 'point', - }, - type: TOKEN_MARK, - data: { className }, - }); - - return myDec; -} diff --git a/packages/grafana-ui/src/slate-plugins/slate-prism/options.tsx b/packages/grafana-ui/src/slate-plugins/slate-prism/options.tsx deleted file mode 100644 index 82320a5a132..00000000000 --- a/packages/grafana-ui/src/slate-plugins/slate-prism/options.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import { Mark, Node, Decoration } from 'slate'; -import { Editor } from '@grafana/slate-react'; -import { Record } from 'immutable'; - -import TOKEN_MARK from './TOKEN_MARK'; - -export interface OptionsFormat { - // Determine which node should be highlighted - onlyIn?: (node: Node) => boolean; - // Returns the syntax for a node that should be highlighted - getSyntax?: (node: Node) => string; - // Render a highlighting mark in a highlighted node - renderMark?: ({ mark, children }: { mark: Mark; children: React.ReactNode }) => void | React.ReactNode; -} - -/** - * Default filter for code blocks - */ -function defaultOnlyIn(node: Node): boolean { - return node.object === 'block' && node.type === 'code_block'; -} - -/** - * Default getter for syntax - */ -function defaultGetSyntax(node: Node): string { - return 'javascript'; -} - -/** - * Default rendering for decorations - */ -function defaultRenderDecoration( - props: { children: React.ReactNode; decoration: Decoration }, - editor: Editor, - next: () => any -): void | React.ReactNode { - const { decoration } = props; - if (decoration.type !== TOKEN_MARK) { - return next(); - } - - const className = decoration.data.get('className'); - return {props.children}; -} - -/** - * The plugin options - */ -class Options - extends Record({ - onlyIn: defaultOnlyIn, - getSyntax: defaultGetSyntax, - renderDecoration: defaultRenderDecoration, - }) - implements OptionsFormat { - readonly onlyIn!: (node: Node) => boolean; - readonly getSyntax!: (node: Node) => string; - readonly renderDecoration!: ( - { - decoration, - children, - }: { - decoration: Decoration; - children: React.ReactNode; - }, - editor: Editor, - next: () => any - ) => void | React.ReactNode; - - constructor(props: OptionsFormat) { - super(props); - } -} - -export default Options; diff --git a/packages/grafana-ui/src/utils/slate.ts b/packages/grafana-ui/src/utils/slate.ts index fcff5e43107..e8a8dd71295 100644 --- a/packages/grafana-ui/src/utils/slate.ts +++ b/packages/grafana-ui/src/utils/slate.ts @@ -1,22 +1,22 @@ -import { Block, Document, Text, Value, SchemaProperties } from 'slate'; +// @ts-ignore +import { Block, Document, Text, Value } from 'slate'; -export const SCHEMA: SchemaProperties = { - document: { - nodes: [ - { - match: [{ type: 'paragraph' }, { type: 'code_block' }, { type: 'code_line' }], - }, - ], +const SCHEMA = { + blocks: { + paragraph: 'paragraph', + codeblock: 'code_block', + codeline: 'code_line', }, inlines: {}, + marks: {}, }; -export const makeFragment = (text: string, syntax?: string): Document => { +export const makeFragment = (text: string, syntax?: string) => { const lines = text.split('\n').map(line => Block.create({ type: 'code_line', nodes: [Text.create(line)], - }) + } as any) ); const block = Block.create({ @@ -25,17 +25,18 @@ export const makeFragment = (text: string, syntax?: string): Document => { }, type: 'code_block', nodes: lines, - }); + } as any); return Document.create({ nodes: [block], }); }; -export const makeValue = (text: string, syntax?: string): Value => { +export const makeValue = (text: string, syntax?: string) => { const fragment = makeFragment(text, syntax); return Value.create({ document: fragment, - }); + SCHEMA, + } as any); }; diff --git a/packages/grafana-ui/tsconfig.json b/packages/grafana-ui/tsconfig.json index 883bbe99ab1..d6dbfc1e0b7 100644 --- a/packages/grafana-ui/tsconfig.json +++ b/packages/grafana-ui/tsconfig.json @@ -5,10 +5,6 @@ "compilerOptions": { "rootDirs": [".", "stories"], "typeRoots": ["./node_modules/@types", "types"], - "baseUrl": "./node_modules/@types", - "paths": { - "@grafana/slate-react": ["slate-react"] - }, "declarationDir": "dist", "outDir": "compiled" } diff --git a/public/app/features/explore/QueryField.test.tsx b/public/app/features/explore/QueryField.test.tsx index e09f00a7b9e..274de4e7ceb 100644 --- a/public/app/features/explore/QueryField.test.tsx +++ b/public/app/features/explore/QueryField.test.tsx @@ -17,4 +17,45 @@ describe('', () => { const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); + + it('should execute query when enter is pressed and there are no suggestions visible', () => { + const wrapper = shallow(); + const instance = wrapper.instance() as QueryField; + instance.executeOnChangeAndRunQueries = jest.fn(); + const handleEnterAndTabKeySpy = jest.spyOn(instance, 'handleEnterKey'); + instance.onKeyDown({ key: 'Enter', preventDefault: () => {} } as KeyboardEvent, {}); + expect(handleEnterAndTabKeySpy).toBeCalled(); + expect(instance.executeOnChangeAndRunQueries).toBeCalled(); + }); + + it('should copy selected text', () => { + const wrapper = shallow(); + const instance = wrapper.instance() as QueryField; + const textBlocks = ['ignore this text. copy this text']; + const copiedText = instance.getCopiedText(textBlocks, 18, 32); + + expect(copiedText).toBe('copy this text'); + }); + + it('should copy selected text across 2 lines', () => { + const wrapper = shallow(); + const instance = wrapper.instance() as QueryField; + const textBlocks = ['ignore this text. start copying here', 'lorem ipsum. stop copying here. lorem ipsum']; + const copiedText = instance.getCopiedText(textBlocks, 18, 30); + + expect(copiedText).toBe('start copying here\nlorem ipsum. stop copying here'); + }); + + it('should copy selected text across > 2 lines', () => { + const wrapper = shallow(); + const instance = wrapper.instance() as QueryField; + const textBlocks = [ + 'ignore this text. start copying here', + 'lorem ipsum doler sit amet', + 'lorem ipsum. stop copying here. lorem ipsum', + ]; + const copiedText = instance.getCopiedText(textBlocks, 18, 30); + + expect(copiedText).toBe('start copying here\nlorem ipsum doler sit amet\nlorem ipsum. stop copying here'); + }); }); diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index d2d58055c5f..8a61a4397e8 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -1,36 +1,55 @@ import _ from 'lodash'; import React, { Context } from 'react'; - -import { Value, Editor as CoreEditor } from 'slate'; -import { Editor, Plugin } from '@grafana/slate-react'; +import ReactDOM from 'react-dom'; +// @ts-ignore +import { Change, Range, Value, Block } from 'slate'; +// @ts-ignore +import { Editor } from 'slate-react'; +// @ts-ignore import Plain from 'slate-plain-serializer'; import classnames from 'classnames'; +// @ts-ignore +import { isKeyHotkey } from 'is-hotkey'; -import { CompletionItemGroup, TypeaheadOutput } from 'app/types/explore'; +import { CompletionItem, CompletionItemGroup, TypeaheadOutput } from 'app/types/explore'; import ClearPlugin from './slate-plugins/clear'; import NewlinePlugin from './slate-plugins/newline'; -import SelectionShortcutsPlugin from './slate-plugins/selection_shortcuts'; -import IndentationPlugin from './slate-plugins/indentation'; -import ClipboardPlugin from './slate-plugins/clipboard'; -import RunnerPlugin from './slate-plugins/runner'; -import SuggestionsPlugin, { SuggestionsState } from './slate-plugins/suggestions'; - -import { Typeahead } from './Typeahead'; -import { makeValue, SCHEMA } from '@grafana/ui'; +import { TypeaheadWithTheme } from './Typeahead'; +import { makeFragment, makeValue } from '@grafana/ui'; +export const TYPEAHEAD_DEBOUNCE = 100; export const HIGHLIGHT_WAIT = 500; +const SLATE_TAB = ' '; +const isIndentLeftHotkey = isKeyHotkey('mod+['); +const isIndentRightHotkey = isKeyHotkey('mod+]'); +const isSelectLeftHotkey = isKeyHotkey('shift+left'); +const isSelectRightHotkey = isKeyHotkey('shift+right'); +const isSelectUpHotkey = isKeyHotkey('shift+up'); +const isSelectDownHotkey = isKeyHotkey('shift+down'); +const isSelectLineHotkey = isKeyHotkey('mod+l'); + +function getSuggestionByIndex(suggestions: CompletionItemGroup[], index: number): CompletionItem { + // Flatten suggestion groups + const flattenedSuggestions = suggestions.reduce((acc, g) => acc.concat(g.items), []); + const correctedIndex = Math.max(index, 0) % flattenedSuggestions.length; + return flattenedSuggestions[correctedIndex]; +} + +function hasSuggestions(suggestions: CompletionItemGroup[]): boolean { + return suggestions && suggestions.length > 0; +} export interface QueryFieldProps { - additionalPlugins?: Plugin[]; + additionalPlugins?: any[]; cleanText?: (text: string) => string; disabled?: boolean; initialQuery: string | null; onRunQuery?: () => void; onChange?: (value: string) => void; - onTypeahead?: (typeahead: TypeaheadInput) => Promise; - onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string; + onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; + onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string; placeholder?: string; portalOrigin?: string; syntax?: string; @@ -40,19 +59,20 @@ export interface QueryFieldProps { export interface QueryFieldState { suggestions: CompletionItemGroup[]; typeaheadContext: string | null; + typeaheadIndex: number; typeaheadPrefix: string; typeaheadText: string; - value: Value; + value: any; lastExecutedValue: Value; } export interface TypeaheadInput { + editorNode: Element; prefix: string; selection?: Selection; text: string; value: Value; - wrapperClasses: string[]; - labelKey?: string; + wrapperNode: Element; } /** @@ -63,35 +83,23 @@ export interface TypeaheadInput { */ export class QueryField extends React.PureComponent { menuEl: HTMLElement | null; - plugins: Plugin[]; - resetTimer: NodeJS.Timer; + plugins: any[]; + resetTimer: any; mounted: boolean; - updateHighlightsTimer: Function; - editor: Editor; - typeaheadRef: Typeahead; + updateHighlightsTimer: any; constructor(props: QueryFieldProps, context: Context) { super(props, context); this.updateHighlightsTimer = _.debounce(this.updateLogsHighlights, HIGHLIGHT_WAIT); - const { onTypeahead, cleanText, portalOrigin, onWillApplySuggestion } = props; - // Base plugins - this.plugins = [ - SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion, component: this }), - ClearPlugin(), - RunnerPlugin({ handler: this.executeOnChangeAndRunQueries }), - NewlinePlugin(), - SelectionShortcutsPlugin(), - IndentationPlugin(), - ClipboardPlugin(), - ...(props.additionalPlugins || []), - ].filter(p => p); + this.plugins = [ClearPlugin(), NewlinePlugin(), ...(props.additionalPlugins || [])].filter(p => p); this.state = { suggestions: [], typeaheadContext: null, + typeaheadIndex: 0, typeaheadPrefix: '', typeaheadText: '', value: makeValue(props.initialQuery || '', props.syntax), @@ -101,6 +109,7 @@ export class QueryField extends React.PureComponent { + onChange = ({ value }: Change, invokeParentOnValueChanged?: boolean) => { const documentChanged = value.document !== this.state.value.document; const prevValue = this.state.value; @@ -145,6 +163,14 @@ export class QueryField extends React.PureComponent { @@ -168,18 +194,475 @@ export class QueryField extends React.PureComponent { + handleTypeahead = _.debounce(async () => { + const selection = window.getSelection(); + const { cleanText, onTypeahead } = this.props; + const { value } = this.state; + + if (onTypeahead && selection.anchorNode) { + const wrapperNode = selection.anchorNode.parentElement; + const editorNode = wrapperNode.closest('.slate-query-field'); + if (!editorNode || this.state.value.isBlurred) { + // Not inside this editor + return; + } + + const range = selection.getRangeAt(0); + const offset = range.startOffset; + const text = selection.anchorNode.textContent; + let prefix = text.substr(0, offset); + + // Label values could have valid characters erased if `cleanText()` is + // blindly applied, which would undesirably interfere with suggestions + const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/); + if (labelValueMatch) { + prefix = labelValueMatch[1]; + } else if (cleanText) { + prefix = cleanText(prefix); + } + + const { suggestions, context, refresher } = onTypeahead({ + editorNode, + prefix, + selection, + text, + value, + wrapperNode, + }); + + let filteredSuggestions = suggestions + .map(group => { + if (group.items) { + if (prefix) { + // Filter groups based on prefix + if (!group.skipFilter) { + group.items = group.items.filter(c => (c.filterText || c.label).length >= prefix.length); + if (group.prefixMatch) { + group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) === 0); + } else { + group.items = group.items.filter(c => (c.filterText || c.label).indexOf(prefix) > -1); + } + } + // Filter out the already typed value (prefix) unless it inserts custom text + group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix); + } + + if (!group.skipSort) { + group.items = _.sortBy(group.items, (item: CompletionItem) => item.sortText || item.label); + } + } + return group; + }) + .filter(group => group.items && group.items.length > 0); // Filter out empty groups + + // Keep same object for equality checking later + if (_.isEqual(filteredSuggestions, this.state.suggestions)) { + filteredSuggestions = this.state.suggestions; + } + + this.setState( + { + suggestions: filteredSuggestions, + typeaheadPrefix: prefix, + typeaheadContext: context, + typeaheadText: text, + }, + () => { + if (refresher) { + refresher.then(this.handleTypeahead).catch(e => console.error(e)); + } + } + ); + } + }, TYPEAHEAD_DEBOUNCE); + + applyTypeahead(change: Change, suggestion: CompletionItem): Change { + const { cleanText, onWillApplySuggestion, syntax } = this.props; + const { typeaheadPrefix, typeaheadText } = this.state; + let suggestionText = suggestion.insertText || suggestion.label; + const preserveSuffix = suggestion.kind === 'function'; + const move = suggestion.move || 0; + + if (onWillApplySuggestion) { + suggestionText = onWillApplySuggestion(suggestionText, { ...this.state }); + } + + this.resetTypeahead(); + + // Remove the current, incomplete text and replace it with the selected suggestion + const backward = suggestion.deleteBackwards || typeaheadPrefix.length; + const text = cleanText ? cleanText(typeaheadText) : typeaheadText; + const suffixLength = text.length - typeaheadPrefix.length; + const offset = typeaheadText.indexOf(typeaheadPrefix); + const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText); + const forward = midWord && !preserveSuffix ? suffixLength + offset : 0; + + // If new-lines, apply suggestion as block + if (suggestionText.match(/\n/)) { + const fragment = makeFragment(suggestionText, syntax); + return change + .deleteBackward(backward) + .deleteForward(forward) + .insertFragment(fragment) + .focus(); + } + + return change + .deleteBackward(backward) + .deleteForward(forward) + .insertText(suggestionText) + .move(move) + .focus(); + } + + handleEnterKey = (event: KeyboardEvent, change: Change) => { + event.preventDefault(); + + if (event.shiftKey) { + // pass through if shift is pressed + return undefined; + } else if (!this.menuEl) { + this.executeOnChangeAndRunQueries(); + return true; + } else { + return this.selectSuggestion(change); + } + }; + + selectSuggestion = (change: Change) => { + const { typeaheadIndex, suggestions } = this.state; + event.preventDefault(); + + if (!suggestions || suggestions.length === 0) { + return undefined; + } + + const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex); + const nextChange = this.applyTypeahead(change, suggestion); + + const insertTextOperation = nextChange.operations.find((operation: any) => operation.type === 'insert_text'); + return insertTextOperation ? true : undefined; + }; + + handleTabKey = (change: Change): void => { + const { + startBlock, + endBlock, + selection: { startOffset, startKey, endOffset, endKey }, + } = change.value; + + if (this.menuEl) { + this.selectSuggestion(change); + return; + } + + const first = startBlock.getFirstText(); + + const startBlockIsSelected = + startOffset === 0 && startKey === first.key && endOffset === first.text.length && endKey === first.key; + + if (startBlockIsSelected || !startBlock.equals(endBlock)) { + this.handleIndent(change, 'right'); + } else { + change.insertText(SLATE_TAB); + } + }; + + handleIndent = (change: Change, indentDirection: 'left' | 'right') => { + const curSelection = change.value.selection; + const selectedBlocks = change.value.document.getBlocksAtRange(curSelection); + + if (indentDirection === 'left') { + for (const block of selectedBlocks) { + const blockWhitespace = block.text.length - block.text.trimLeft().length; + + const rangeProperties = { + anchorKey: block.getFirstText().key, + anchorOffset: blockWhitespace, + focusKey: block.getFirstText().key, + focusOffset: blockWhitespace, + }; + + // @ts-ignore + const whitespaceToDelete = Range.create(rangeProperties); + + change.deleteBackwardAtRange(whitespaceToDelete, Math.min(SLATE_TAB.length, blockWhitespace)); + } + } else { + const { startText } = change.value; + const textBeforeCaret = startText.text.slice(0, curSelection.startOffset); + const isWhiteSpace = /^\s*$/.test(textBeforeCaret); + + for (const block of selectedBlocks) { + change.insertTextByKey(block.getFirstText().key, 0, SLATE_TAB); + } + + if (isWhiteSpace) { + change.moveStart(-SLATE_TAB.length); + } + } + }; + + handleSelectVertical = (change: Change, direction: 'up' | 'down') => { + const { focusBlock } = change.value; + const adjacentBlock = + direction === 'up' + ? change.value.document.getPreviousBlock(focusBlock.key) + : change.value.document.getNextBlock(focusBlock.key); + + if (!adjacentBlock) { + return true; + } + const adjacentText = adjacentBlock.getFirstText(); + change.moveFocusTo(adjacentText.key, Math.min(change.value.anchorOffset, adjacentText.text.length)).focus(); + return true; + }; + + handleSelectUp = (change: Change) => this.handleSelectVertical(change, 'up'); + + handleSelectDown = (change: Change) => this.handleSelectVertical(change, 'down'); + + onKeyDown = (event: KeyboardEvent, change: Change) => { + const { typeaheadIndex } = this.state; + + // Shortcuts + if (isIndentLeftHotkey(event)) { + event.preventDefault(); + this.handleIndent(change, 'left'); + return true; + } else if (isIndentRightHotkey(event)) { + event.preventDefault(); + this.handleIndent(change, 'right'); + return true; + } else if (isSelectLeftHotkey(event)) { + event.preventDefault(); + if (change.value.focusOffset > 0) { + change.moveFocus(-1); + } + return true; + } else if (isSelectRightHotkey(event)) { + event.preventDefault(); + if (change.value.focusOffset < change.value.startText.text.length) { + change.moveFocus(1); + } + return true; + } else if (isSelectUpHotkey(event)) { + event.preventDefault(); + this.handleSelectUp(change); + return true; + } else if (isSelectDownHotkey(event)) { + event.preventDefault(); + this.handleSelectDown(change); + return true; + } else if (isSelectLineHotkey(event)) { + event.preventDefault(); + const { focusBlock, document } = change.value; + + change.moveAnchorToStartOfBlock(focusBlock.key); + + const nextBlock = document.getNextBlock(focusBlock.key); + if (nextBlock) { + change.moveFocusToStartOfNextBlock(); + } else { + change.moveFocusToEndOfText(); + } + + return true; + } + + switch (event.key) { + case 'Escape': { + if (this.menuEl) { + event.preventDefault(); + event.stopPropagation(); + this.resetTypeahead(); + return true; + } + break; + } + + case ' ': { + if (event.ctrlKey) { + event.preventDefault(); + this.handleTypeahead(); + return true; + } + break; + } + + case 'Enter': + return this.handleEnterKey(event, change); + + case 'Tab': { + event.preventDefault(); + return this.handleTabKey(change); + } + + case 'ArrowDown': { + if (this.menuEl) { + // Select next suggestion + event.preventDefault(); + const itemsCount = + this.state.suggestions.length > 0 + ? this.state.suggestions.reduce((totalCount, current) => totalCount + current.items.length, 0) + : 0; + this.setState({ typeaheadIndex: Math.min(itemsCount - 1, typeaheadIndex + 1) }); + } + break; + } + + case 'ArrowUp': { + if (this.menuEl) { + // Select previous suggestion + event.preventDefault(); + this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) }); + } + break; + } + + default: { + // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key); + break; + } + } + return undefined; + }; + + resetTypeahead = () => { + if (this.mounted) { + this.setState({ suggestions: [], typeaheadIndex: 0, typeaheadPrefix: '', typeaheadContext: null }); + this.resetTimer = null; + } + }; + + handleBlur = (event: FocusEvent, change: Change) => { const { lastExecutedValue } = this.state; const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null; - const currentValue = Plain.serialize(editor.value); + const currentValue = Plain.serialize(change.value); + + // If we dont wait here, menu clicks wont work because the menu + // will be gone. + this.resetTimer = setTimeout(this.resetTypeahead, 100); if (previousValue !== currentValue) { this.executeOnChangeAndRunQueries(); } + }; + + onClickMenu = (item: CompletionItem) => { + // Manually triggering change + const change = this.applyTypeahead(this.state.value.change(), item); + this.onChange(change, true); + }; + + updateMenu = () => { + const { suggestions } = this.state; + const menu = this.menuEl; + // Exit for unit tests + if (!window.getSelection) { + return; + } + const selection = window.getSelection(); + const node = selection.anchorNode; + + // No menu, nothing to do + if (!menu) { + return; + } + + // No suggestions or blur, remove menu + if (!hasSuggestions(suggestions)) { + menu.removeAttribute('style'); + return; + } + + // Align menu overlay to editor node + if (node) { + // Read from DOM + const rect = node.parentElement.getBoundingClientRect(); + const scrollX = window.scrollX; + const scrollY = window.scrollY; + + // Write DOM + requestAnimationFrame(() => { + menu.style.opacity = '1'; + menu.style.top = `${rect.top + scrollY + rect.height + 4}px`; + menu.style.left = `${rect.left + scrollX - 2}px`; + }); + } + }; + + menuRef = (el: HTMLElement) => { + this.menuEl = el; + }; + + renderMenu = () => { + const { portalOrigin } = this.props; + const { suggestions, typeaheadIndex, typeaheadPrefix } = this.state; + if (!hasSuggestions(suggestions)) { + return null; + } + + const selectedItem = getSuggestionByIndex(suggestions, typeaheadIndex); + + // Create typeahead in DOM root so we can later position it absolutely + return ( + + + + ); + }; + + getCopiedText(textBlocks: string[], startOffset: number, endOffset: number) { + if (!textBlocks.length) { + return undefined; + } + + const excludingLastLineLength = textBlocks.slice(0, -1).join('').length + textBlocks.length - 1; + return textBlocks.join('\n').slice(startOffset, excludingLastLineLength + endOffset); + } + + handleCopy = (event: ClipboardEvent, change: Change) => { + event.preventDefault(); + + const { document, selection, startOffset, endOffset } = change.value; + const selectedBlocks = document.getBlocksAtRangeAsArray(selection).map((block: Block) => block.text); + + const copiedText = this.getCopiedText(selectedBlocks, startOffset, endOffset); + if (copiedText) { + event.clipboardData.setData('Text', copiedText); + } + + return true; + }; + + handlePaste = (event: ClipboardEvent, change: Change) => { + event.preventDefault(); + const pastedValue = event.clipboardData.getData('Text'); + const lines = pastedValue.split('\n'); + + if (lines.length) { + change.insertText(lines[0]); + for (const line of lines.slice(1)) { + change.splitBlock().insertText(line); + } + } + + return true; + }; - editor.blur(); + handleCut = (event: ClipboardEvent, change: Change) => { + this.handleCopy(event, change); + change.deleteAtRange(change.value.selection); - return next(); + return true; }; render() { @@ -187,20 +670,19 @@ export class QueryField extends React.PureComponent
+ {this.renderMenu()} (this.editor = editor)} - schema={SCHEMA} autoCorrect={false} readOnly={this.props.disabled} onBlur={this.handleBlur} - // onKeyDown={this.onKeyDown} - onChange={(change: { value: Value }) => { - this.onChange(change.value, false); - }} + onKeyDown={this.onKeyDown} + onChange={this.onChange} + onCopy={this.handleCopy} + onPaste={this.handlePaste} + onCut={this.handleCut} placeholder={this.props.placeholder} plugins={this.plugins} spellCheck={false} @@ -212,4 +694,29 @@ export class QueryField extends React.PureComponent { + node: HTMLElement; + + constructor(props: PortalProps) { + super(props); + const { index = 0, origin = 'query' } = props; + this.node = document.createElement('div'); + this.node.classList.add(`slate-typeahead`, `slate-typeahead-${origin}-${index}`); + document.body.appendChild(this.node); + } + + componentWillUnmount() { + document.body.removeChild(this.node); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.node); + } +} + export default QueryField; diff --git a/public/app/features/explore/Typeahead.tsx b/public/app/features/explore/Typeahead.tsx index 91e675d7eb8..b28ab4a610d 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/public/app/features/explore/Typeahead.tsx @@ -1,24 +1,21 @@ -import React, { createRef, CSSProperties } from 'react'; -import ReactDOM from 'react-dom'; +import React, { createRef } from 'react'; import _ from 'lodash'; import { FixedSizeList } from 'react-window'; import { Themeable, withTheme } from '@grafana/ui'; -import { CompletionItem, CompletionItemKind, CompletionItemGroup } from 'app/types/explore'; +import { CompletionItem, CompletionItemGroup } from 'app/types/explore'; import { TypeaheadItem } from './TypeaheadItem'; import { TypeaheadInfo } from './TypeaheadInfo'; import { flattenGroupItems, calculateLongestLabel, calculateListSizes } from './utils/typeahead'; -const modulo = (a: number, n: number) => a - n * Math.floor(a / n); - interface Props extends Themeable { - origin: string; groupedItems: CompletionItemGroup[]; + menuRef: any; + selectedItem: CompletionItem | null; + onClickItem: (suggestion: CompletionItem) => void; prefix?: string; - menuRef?: (el: Typeahead) => void; - onSelectSuggestion?: (suggestion: CompletionItem) => void; - isOpen?: boolean; + typeaheadIndex: number; } interface State { @@ -26,12 +23,11 @@ interface State { listWidth: number; listHeight: number; itemHeight: number; - hoveredItem: number; - typeaheadIndex: number; } export class Typeahead extends React.PureComponent { - listRef = createRef(); + listRef: any = createRef(); + documentationRef: any = createRef(); constructor(props: Props) { super(props); @@ -39,173 +35,97 @@ export class Typeahead extends React.PureComponent { const allItems = flattenGroupItems(props.groupedItems); const longestLabel = calculateLongestLabel(allItems); const { listWidth, listHeight, itemHeight } = calculateListSizes(props.theme, allItems, longestLabel); - this.state = { listWidth, listHeight, itemHeight, hoveredItem: null, typeaheadIndex: 1, allItems }; + this.state = { listWidth, listHeight, itemHeight, allItems }; } - componentDidMount = () => { - this.props.menuRef(this); - }; - - componentDidUpdate = (prevProps: Readonly, prevState: Readonly) => { - if (prevState.typeaheadIndex !== this.state.typeaheadIndex && this.listRef && this.listRef.current) { - if (this.state.typeaheadIndex === 1) { + componentDidUpdate = (prevProps: Readonly) => { + if (prevProps.typeaheadIndex !== this.props.typeaheadIndex && this.listRef && this.listRef.current) { + if (prevProps.typeaheadIndex === 1 && this.props.typeaheadIndex === 0) { this.listRef.current.scrollToItem(0); // special case for handling the first group label + this.refreshDocumentation(); return; } - this.listRef.current.scrollToItem(this.state.typeaheadIndex); + const index = this.state.allItems.findIndex(item => item === this.props.selectedItem); + this.listRef.current.scrollToItem(index); + this.refreshDocumentation(); } if (_.isEqual(prevProps.groupedItems, this.props.groupedItems) === false) { const allItems = flattenGroupItems(this.props.groupedItems); const longestLabel = calculateLongestLabel(allItems); const { listWidth, listHeight, itemHeight } = calculateListSizes(this.props.theme, allItems, longestLabel); - this.setState({ listWidth, listHeight, itemHeight, allItems }); + this.setState({ listWidth, listHeight, itemHeight, allItems }, () => this.refreshDocumentation()); } }; - onMouseEnter = (index: number) => { - this.setState({ - hoveredItem: index, - }); - }; - - onMouseLeave = () => { - this.setState({ - hoveredItem: null, - }); - }; - - moveMenuIndex = (moveAmount: number) => { - const itemCount = this.state.allItems.length; - if (itemCount) { - // Select next suggestion - event.preventDefault(); - let newTypeaheadIndex = modulo(this.state.typeaheadIndex + moveAmount, itemCount); - - if (this.state.allItems[newTypeaheadIndex].kind === CompletionItemKind.GroupTitle) { - newTypeaheadIndex = modulo(newTypeaheadIndex + moveAmount, itemCount); - } - - this.setState({ - typeaheadIndex: newTypeaheadIndex, - }); - + refreshDocumentation = () => { + if (!this.documentationRef.current) { return; } - }; - insertSuggestion = () => { - this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]); - }; + const index = this.state.allItems.findIndex(item => item === this.props.selectedItem); + const item = this.state.allItems[index]; - get menuPosition(): CSSProperties { - // Exit for unit tests - if (!window.getSelection) { - return {}; + if (item) { + this.documentationRef.current.refresh(item); } + }; - const selection = window.getSelection(); - const node = selection.anchorNode; - - // Align menu overlay to editor node - if (node) { - // Read from DOM - const rect = node.parentElement.getBoundingClientRect(); - const scrollX = window.scrollX; - const scrollY = window.scrollY; - - return { - top: `${rect.top + scrollY + rect.height + 4}px`, - left: `${rect.left + scrollX - 2}px`, - }; - } + onMouseEnter = (item: CompletionItem) => { + this.documentationRef.current.refresh(item); + }; - return {}; - } + onMouseLeave = () => { + this.documentationRef.current.hide(); + }; render() { - const { prefix, theme, isOpen, origin } = this.props; - const { allItems, listWidth, listHeight, itemHeight, hoveredItem, typeaheadIndex } = this.state; - - const showDocumentation = hoveredItem || typeaheadIndex; + const { menuRef, selectedItem, onClickItem, prefix, theme } = this.props; + const { listWidth, listHeight, itemHeight, allItems } = this.state; return ( - -
    - { - const item = allItems && allItems[index]; - const key = item ? `${index}-${item.label}` : `${index}`; - return key; - }} - width={listWidth} - height={listHeight} - > - {({ index, style }) => { - const item = allItems && allItems[index]; - if (!item) { - return null; - } - - return ( - this.props.onSelectSuggestion(item)} - isSelected={allItems[typeaheadIndex] === item} - item={item} - prefix={prefix} - style={style} - onMouseEnter={() => this.onMouseEnter(index)} - onMouseLeave={this.onMouseLeave} - /> - ); - }} - -
- - {showDocumentation && ( - - )} -
+
    + + { + const item = allItems && allItems[index]; + const key = item ? `${index}-${item.label}` : `${index}`; + return key; + }} + width={listWidth} + height={listHeight} + > + {({ index, style }) => { + const item = allItems && allItems[index]; + if (!item) { + return null; + } + + return ( + + ); + }} + +
); } } export const TypeaheadWithTheme = withTheme(Typeahead); - -interface PortalProps { - index?: number; - isOpen: boolean; - origin: string; -} - -class Portal extends React.PureComponent { - node: HTMLElement; - - constructor(props: PortalProps) { - super(props); - const { index = 0, origin = 'query' } = props; - this.node = document.createElement('div'); - this.node.classList.add(`slate-typeahead`, `slate-typeahead-${origin}-${index}`); - document.body.appendChild(this.node); - } - - componentWillUnmount() { - document.body.removeChild(this.node); - } - - render() { - if (this.props.isOpen) { - return ReactDOM.createPortal(this.props.children, this.node); - } - - return null; - } -} diff --git a/public/app/features/explore/TypeaheadInfo.tsx b/public/app/features/explore/TypeaheadInfo.tsx index f18edcee17d..4b410c8b365 100644 --- a/public/app/features/explore/TypeaheadInfo.tsx +++ b/public/app/features/explore/TypeaheadInfo.tsx @@ -1,26 +1,29 @@ import React, { PureComponent } from 'react'; -import { css, cx } from 'emotion'; - import { Themeable, selectThemeVariant } from '@grafana/ui'; +import { css, cx } from 'emotion'; import { CompletionItem } from 'app/types/explore'; interface Props extends Themeable { - item: CompletionItem; + initialItem: CompletionItem; width: number; height: number; } -export class TypeaheadInfo extends PureComponent { +interface State { + item: CompletionItem; +} + +export class TypeaheadInfo extends PureComponent { constructor(props: Props) { super(props); + this.state = { item: props.initialItem }; } getStyles = (visible: boolean) => { const { width, height, theme } = this.props; const selection = window.getSelection(); const node = selection.anchorNode; - if (!node) { return {}; } @@ -35,7 +38,7 @@ export class TypeaheadInfo extends PureComponent { return { typeaheadItem: css` label: type-ahead-item; - z-index: 500; + z-index: auto; padding: ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.md}; border-radius: ${theme.border.radius.md}; border: ${selectThemeVariant( @@ -61,8 +64,16 @@ export class TypeaheadInfo extends PureComponent { }; }; + refresh = (item: CompletionItem) => { + this.setState({ item }); + }; + + hide = () => { + this.setState({ item: null }); + }; + render() { - const { item } = this.props; + const { item } = this.state; const visible = item && !!item.documentation; const label = item ? item.label : ''; const documentation = item && item.documentation ? item.documentation : ''; diff --git a/public/app/features/explore/TypeaheadItem.tsx b/public/app/features/explore/TypeaheadItem.tsx index e20a5758613..f670330e44d 100644 --- a/public/app/features/explore/TypeaheadItem.tsx +++ b/public/app/features/explore/TypeaheadItem.tsx @@ -1,21 +1,25 @@ import React, { FunctionComponent, useContext } from 'react'; - // @ts-ignore import Highlighter from 'react-highlight-words'; import { css, cx } from 'emotion'; import { GrafanaTheme, ThemeContext, selectThemeVariant } from '@grafana/ui'; -import { CompletionItem, CompletionItemKind } from 'app/types/explore'; +import { CompletionItem } from 'app/types/explore'; + +export const GROUP_TITLE_KIND = 'GroupTitle'; + +export const isGroupTitle = (item: CompletionItem) => { + return item.kind && item.kind === GROUP_TITLE_KIND ? true : false; +}; interface Props { isSelected: boolean; item: CompletionItem; - style: any; + onClickItem: (suggestion: CompletionItem) => void; prefix?: string; - - onClickItem?: (event: React.MouseEvent) => void; - onMouseEnter?: () => void; - onMouseLeave?: () => void; + style: any; + onMouseEnter: (item: CompletionItem) => void; + onMouseLeave: (item: CompletionItem) => void; } const getStyles = (theme: GrafanaTheme) => ({ @@ -34,12 +38,10 @@ const getStyles = (theme: GrafanaTheme) => ({ transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1); `, - typeaheadItemSelected: css` label: type-ahead-item-selected; background-color: ${selectThemeVariant({ light: theme.colors.gray6, dark: theme.colors.dark9 }, theme.type)}; `, - typeaheadItemMatch: css` label: type-ahead-item-match; color: ${theme.colors.yellow}; @@ -47,7 +49,6 @@ const getStyles = (theme: GrafanaTheme) => ({ padding: inherit; background: inherit; `, - typeaheadItemGroupTitle: css` label: type-ahead-item-group-title; color: ${theme.colors.textWeak}; @@ -61,13 +62,16 @@ export const TypeaheadItem: FunctionComponent = (props: Props) => { const theme = useContext(ThemeContext); const styles = getStyles(theme); - const { isSelected, item, prefix, style, onMouseEnter, onMouseLeave, onClickItem } = props; + const { isSelected, item, prefix, style, onClickItem } = props; + const onClick = () => onClickItem(item); + const onMouseEnter = () => props.onMouseEnter(item); + const onMouseLeave = () => props.onMouseLeave(item); const className = isSelected ? cx([styles.typeaheadItem, styles.typeaheadItemSelected]) : cx([styles.typeaheadItem]); const highlightClassName = cx([styles.typeaheadItemMatch]); const itemGroupTitleClassName = cx([styles.typeaheadItemGroupTitle]); const label = item.label || ''; - if (item.kind === CompletionItemKind.GroupTitle) { + if (isGroupTitle(item)) { return (
  • {label} @@ -76,13 +80,7 @@ export const TypeaheadItem: FunctionComponent = (props: Props) => { } return ( -
  • +
  • ); diff --git a/public/app/features/explore/slate-plugins/braces.test.ts b/public/app/features/explore/slate-plugins/braces.test.ts new file mode 100644 index 00000000000..d72ea0f3d97 --- /dev/null +++ b/public/app/features/explore/slate-plugins/braces.test.ts @@ -0,0 +1,39 @@ +// @ts-ignore +import Plain from 'slate-plain-serializer'; + +import BracesPlugin from './braces'; + +declare global { + interface Window { + KeyboardEvent: any; + } +} + +describe('braces', () => { + const handler = BracesPlugin().onKeyDown; + + it('adds closing braces around empty value', () => { + const change = Plain.deserialize('').change(); + const event = new window.KeyboardEvent('keydown', { key: '(' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('()'); + }); + + it('removes closing brace when opening brace is removed', () => { + const change = Plain.deserialize('time()').change(); + let event; + change.move(5); + event = new window.KeyboardEvent('keydown', { key: 'Backspace' }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('time'); + }); + + it('keeps closing brace when opening brace is removed and inner values exist', () => { + const change = Plain.deserialize('time(value)').change(); + let event; + change.move(5); + event = new window.KeyboardEvent('keydown', { key: 'Backspace' }); + const handled = handler(event, change); + expect(handled).toBeFalsy(); + }); +}); diff --git a/public/app/features/explore/slate-plugins/braces.test.tsx b/public/app/features/explore/slate-plugins/braces.test.tsx deleted file mode 100644 index a80f67c817f..00000000000 --- a/public/app/features/explore/slate-plugins/braces.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import Plain from 'slate-plain-serializer'; -import { Editor } from '@grafana/slate-react'; -import { shallow } from 'enzyme'; -import BracesPlugin from './braces'; - -declare global { - interface Window { - KeyboardEvent: any; - } -} - -describe('braces', () => { - const handler = BracesPlugin().onKeyDown; - const nextMock = () => {}; - - it('adds closing braces around empty value', () => { - const value = Plain.deserialize(''); - const editor = shallow(); - const event = new window.KeyboardEvent('keydown', { key: '(' }); - handler(event as Event, editor.instance() as any, nextMock); - expect(Plain.serialize(editor.instance().value)).toEqual('()'); - }); - - it('removes closing brace when opening brace is removed', () => { - const value = Plain.deserialize('time()'); - const editor = shallow(); - const event = new window.KeyboardEvent('keydown', { key: 'Backspace' }); - handler(event as Event, editor.instance().moveForward(5) as any, nextMock); - expect(Plain.serialize(editor.instance().value)).toEqual('time'); - }); - - it('keeps closing brace when opening brace is removed and inner values exist', () => { - const value = Plain.deserialize('time(value)'); - const editor = shallow(); - const event = new window.KeyboardEvent('keydown', { key: 'Backspace' }); - const handled = handler(event as Event, editor.instance().moveForward(5) as any, nextMock); - expect(handled).toBeFalsy(); - }); -}); diff --git a/public/app/features/explore/slate-plugins/braces.ts b/public/app/features/explore/slate-plugins/braces.ts index 0eff1fa7e4f..ee6227cc309 100644 --- a/public/app/features/explore/slate-plugins/braces.ts +++ b/public/app/features/explore/slate-plugins/braces.ts @@ -1,5 +1,5 @@ -import { Plugin } from '@grafana/slate-react'; -import { Editor as CoreEditor } from 'slate'; +// @ts-ignore +import { Change } from 'slate'; const BRACES: any = { '[': ']', @@ -7,37 +7,34 @@ const BRACES: any = { '(': ')', }; -export default function BracesPlugin(): Plugin { +export default function BracesPlugin() { return { - onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) { - const { value } = editor; + onKeyDown(event: KeyboardEvent, change: Change) { + const { value } = change; switch (event.key) { case '(': case '{': case '[': { event.preventDefault(); - const { - start: { offset: startOffset, key: startKey }, - end: { offset: endOffset, key: endKey }, - focus: { offset: focusOffset }, - } = value.selection; - const text = value.focusText.text; + + const { startOffset, startKey, endOffset, endKey, focusOffset } = value.selection; + const text: string = value.focusText.text; // If text is selected, wrap selected text in parens - if (value.selection.isExpanded) { - editor + if (value.isExpanded) { + change .insertTextByKey(startKey, startOffset, event.key) .insertTextByKey(endKey, endOffset + 1, BRACES[event.key]) - .moveEndBackward(1); + .moveEnd(-1); } else if ( focusOffset === text.length || text[focusOffset] === ' ' || Object.values(BRACES).includes(text[focusOffset]) ) { - editor.insertText(`${event.key}${BRACES[event.key]}`).moveBackward(1); + change.insertText(`${event.key}${BRACES[event.key]}`).move(-1); } else { - editor.insertText(event.key); + change.insertText(event.key); } return true; @@ -45,15 +42,15 @@ export default function BracesPlugin(): Plugin { case 'Backspace': { const text = value.anchorText.text; - const offset = value.selection.anchor.offset; + const offset = value.anchorOffset; const previousChar = text[offset - 1]; const nextChar = text[offset]; if (BRACES[previousChar] && BRACES[previousChar] === nextChar) { event.preventDefault(); // Remove closing brace if directly following - editor - .deleteBackward(1) - .deleteForward(1) + change + .deleteBackward() + .deleteForward() .focus(); return true; } @@ -63,8 +60,7 @@ export default function BracesPlugin(): Plugin { break; } } - - return next(); + return undefined; }, }; } diff --git a/public/app/features/explore/slate-plugins/clear.test.ts b/public/app/features/explore/slate-plugins/clear.test.ts new file mode 100644 index 00000000000..9322fffd7d2 --- /dev/null +++ b/public/app/features/explore/slate-plugins/clear.test.ts @@ -0,0 +1,39 @@ +// @ts-ignore +import Plain from 'slate-plain-serializer'; + +import ClearPlugin from './clear'; + +describe('clear', () => { + const handler = ClearPlugin().onKeyDown; + + it('does not change the empty value', () => { + const change = Plain.deserialize('').change(); + const event = new window.KeyboardEvent('keydown', { + key: 'k', + ctrlKey: true, + }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual(''); + }); + + it('clears to the end of the line', () => { + const change = Plain.deserialize('foo').change(); + const event = new window.KeyboardEvent('keydown', { + key: 'k', + ctrlKey: true, + }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual(''); + }); + + it('clears from the middle to the end of the line', () => { + const change = Plain.deserialize('foo bar').change(); + change.move(4); + const event = new window.KeyboardEvent('keydown', { + key: 'k', + ctrlKey: true, + }); + handler(event, change); + expect(Plain.serialize(change.value)).toEqual('foo '); + }); +}); diff --git a/public/app/features/explore/slate-plugins/clear.test.tsx b/public/app/features/explore/slate-plugins/clear.test.tsx deleted file mode 100644 index 4565827e385..00000000000 --- a/public/app/features/explore/slate-plugins/clear.test.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import Plain from 'slate-plain-serializer'; -import React from 'react'; -import { Editor } from '@grafana/slate-react'; -import { shallow } from 'enzyme'; -import ClearPlugin from './clear'; - -describe('clear', () => { - const handler = ClearPlugin().onKeyDown; - - it('does not change the empty value', () => { - const value = Plain.deserialize(''); - const editor = shallow(); - const event = new window.KeyboardEvent('keydown', { - key: 'k', - ctrlKey: true, - }); - handler(event as Event, editor.instance() as any, () => {}); - expect(Plain.serialize(editor.instance().value)).toEqual(''); - }); - - it('clears to the end of the line', () => { - const value = Plain.deserialize('foo'); - const editor = shallow(); - const event = new window.KeyboardEvent('keydown', { - key: 'k', - ctrlKey: true, - }); - handler(event as Event, editor.instance() as any, () => {}); - expect(Plain.serialize(editor.instance().value)).toEqual(''); - }); - - it('clears from the middle to the end of the line', () => { - const value = Plain.deserialize('foo bar'); - const editor = shallow(); - const event = new window.KeyboardEvent('keydown', { - key: 'k', - ctrlKey: true, - }); - handler(event as Event, editor.instance().moveForward(4) as any, () => {}); - expect(Plain.serialize(editor.instance().value)).toEqual('foo '); - }); -}); diff --git a/public/app/features/explore/slate-plugins/clear.ts b/public/app/features/explore/slate-plugins/clear.ts index 83dcf2e27b7..9d649aa6926 100644 --- a/public/app/features/explore/slate-plugins/clear.ts +++ b/public/app/features/explore/slate-plugins/clear.ts @@ -1,27 +1,22 @@ -import { Plugin } from '@grafana/slate-react'; -import { Editor as CoreEditor } from 'slate'; - // Clears the rest of the line after the caret -export default function ClearPlugin(): Plugin { +export default function ClearPlugin() { return { - onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) { - const value = editor.value; - - if (value.selection.isExpanded) { - return next(); + onKeyDown(event: any, change: { value?: any; deleteForward?: any }) { + const { value } = change; + if (!value.isCollapsed) { + return undefined; } if (event.key === 'k' && event.ctrlKey) { event.preventDefault(); const text = value.anchorText.text; - const offset = value.selection.anchor.offset; + const offset = value.anchorOffset; const length = text.length; const forward = length - offset; - editor.deleteForward(forward); + change.deleteForward(forward); return true; } - - return next(); + return undefined; }, }; } diff --git a/public/app/features/explore/slate-plugins/clipboard.ts b/public/app/features/explore/slate-plugins/clipboard.ts deleted file mode 100644 index 79d277ec65a..00000000000 --- a/public/app/features/explore/slate-plugins/clipboard.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Plugin } from '@grafana/slate-react'; -import { Editor as CoreEditor } from 'slate'; - -const getCopiedText = (textBlocks: string[], startOffset: number, endOffset: number) => { - if (!textBlocks.length) { - return undefined; - } - - const excludingLastLineLength = textBlocks.slice(0, -1).join('').length + textBlocks.length - 1; - return textBlocks.join('\n').slice(startOffset, excludingLastLineLength + endOffset); -}; - -export default function ClipboardPlugin(): Plugin { - const clipboardPlugin = { - onCopy(event: ClipboardEvent, editor: CoreEditor) { - event.preventDefault(); - - const { document, selection } = editor.value; - const { - start: { offset: startOffset }, - end: { offset: endOffset }, - } = selection; - const selectedBlocks = document - .getLeafBlocksAtRange(selection) - .toArray() - .map(block => block.text); - - const copiedText = getCopiedText(selectedBlocks, startOffset, endOffset); - if (copiedText) { - event.clipboardData.setData('Text', copiedText); - } - - return true; - }, - - onPaste(event: ClipboardEvent, editor: CoreEditor) { - event.preventDefault(); - const pastedValue = event.clipboardData.getData('Text'); - const lines = pastedValue.split('\n'); - - if (lines.length) { - editor.insertText(lines[0]); - for (const line of lines.slice(1)) { - editor.splitBlock().insertText(line); - } - } - - return true; - }, - }; - - return { - ...clipboardPlugin, - onCut(event: ClipboardEvent, editor: CoreEditor) { - clipboardPlugin.onCopy(event, editor); - editor.deleteAtRange(editor.value.selection); - - return true; - }, - }; -} diff --git a/public/app/features/explore/slate-plugins/indentation.ts b/public/app/features/explore/slate-plugins/indentation.ts deleted file mode 100644 index d3f1ab154c3..00000000000 --- a/public/app/features/explore/slate-plugins/indentation.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { RangeJSON, Range as SlateRange, Editor as CoreEditor } from 'slate'; -import { Plugin } from '@grafana/slate-react'; -import { isKeyHotkey } from 'is-hotkey'; - -const isIndentLeftHotkey = isKeyHotkey('mod+['); -const isShiftTabHotkey = isKeyHotkey('shift+tab'); -const isIndentRightHotkey = isKeyHotkey('mod+]'); - -const SLATE_TAB = ' '; - -const handleTabKey = (event: KeyboardEvent, editor: CoreEditor, next: Function): void => { - const { - startBlock, - endBlock, - selection: { - start: { offset: startOffset, key: startKey }, - end: { offset: endOffset, key: endKey }, - }, - } = editor.value; - - const first = startBlock.getFirstText(); - - const startBlockIsSelected = - startOffset === 0 && startKey === first.key && endOffset === first.text.length && endKey === first.key; - - if (startBlockIsSelected || !startBlock.equals(endBlock)) { - handleIndent(editor, 'right'); - } else { - editor.insertText(SLATE_TAB); - } -}; - -const handleIndent = (editor: CoreEditor, indentDirection: 'left' | 'right') => { - const curSelection = editor.value.selection; - const selectedBlocks = editor.value.document.getLeafBlocksAtRange(curSelection).toArray(); - - if (indentDirection === 'left') { - for (const block of selectedBlocks) { - const blockWhitespace = block.text.length - block.text.trimLeft().length; - - const textKey = block.getFirstText().key; - - const rangeProperties: RangeJSON = { - anchor: { - key: textKey, - offset: blockWhitespace, - path: [], - }, - focus: { - key: textKey, - offset: blockWhitespace, - path: [], - }, - }; - - editor.deleteBackwardAtRange(SlateRange.create(rangeProperties), Math.min(SLATE_TAB.length, blockWhitespace)); - } - } else { - const { startText } = editor.value; - const textBeforeCaret = startText.text.slice(0, curSelection.start.offset); - const isWhiteSpace = /^\s*$/.test(textBeforeCaret); - - for (const block of selectedBlocks) { - editor.insertTextByKey(block.getFirstText().key, 0, SLATE_TAB); - } - - if (isWhiteSpace) { - editor.moveStartBackward(SLATE_TAB.length); - } - } -}; - -// Clears the rest of the line after the caret -export default function IndentationPlugin(): Plugin { - return { - onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) { - if (isIndentLeftHotkey(event) || isShiftTabHotkey(event)) { - event.preventDefault(); - handleIndent(editor, 'left'); - } else if (isIndentRightHotkey(event)) { - event.preventDefault(); - handleIndent(editor, 'right'); - } else if (event.key === 'Tab') { - event.preventDefault(); - handleTabKey(event, editor, next); - } else { - return next(); - } - - return true; - }, - }; -} diff --git a/public/app/features/explore/slate-plugins/newline.ts b/public/app/features/explore/slate-plugins/newline.ts index c31d2a74b18..a20bb162870 100644 --- a/public/app/features/explore/slate-plugins/newline.ts +++ b/public/app/features/explore/slate-plugins/newline.ts @@ -1,7 +1,7 @@ -import { Plugin } from '@grafana/slate-react'; -import { Editor as CoreEditor } from 'slate'; +// @ts-ignore +import { Change } from 'slate'; -function getIndent(text: string) { +function getIndent(text: any) { let offset = text.length - text.trimLeft().length; if (offset) { let indent = text[0]; @@ -13,13 +13,12 @@ function getIndent(text: string) { return ''; } -export default function NewlinePlugin(): Plugin { +export default function NewlinePlugin() { return { - onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) { - const value = editor.value; - - if (value.selection.isExpanded) { - return next(); + onKeyDown(event: KeyboardEvent, change: Change) { + const { value } = change; + if (!value.isCollapsed) { + return undefined; } if (event.key === 'Enter' && event.shiftKey) { @@ -29,13 +28,11 @@ export default function NewlinePlugin(): Plugin { const currentLineText = startBlock.text; const indent = getIndent(currentLineText); - return editor + return change .splitBlock() .insertText(indent) .focus(); } - - return next(); }, }; } diff --git a/public/app/features/explore/slate-plugins/runner.test.tsx b/public/app/features/explore/slate-plugins/runner.test.tsx deleted file mode 100644 index 3604681e03a..00000000000 --- a/public/app/features/explore/slate-plugins/runner.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import Plain from 'slate-plain-serializer'; -import React from 'react'; -import { Editor } from '@grafana/slate-react'; -import { shallow } from 'enzyme'; -import RunnerPlugin from './runner'; - -describe('runner', () => { - const mockHandler = jest.fn(); - const handler = RunnerPlugin({ handler: mockHandler }).onKeyDown; - - it('should execute query when enter is pressed and there are no suggestions visible', () => { - const value = Plain.deserialize(''); - const editor = shallow(); - handler({ key: 'Enter', preventDefault: () => {} } as KeyboardEvent, editor.instance() as any, () => {}); - expect(mockHandler).toBeCalled(); - }); -}); diff --git a/public/app/features/explore/slate-plugins/runner.ts b/public/app/features/explore/slate-plugins/runner.ts index bb3a10f8759..fc7b8a778ed 100644 --- a/public/app/features/explore/slate-plugins/runner.ts +++ b/public/app/features/explore/slate-plugins/runner.ts @@ -1,8 +1,6 @@ -import { Editor as SlateEditor } from 'slate'; - export default function RunnerPlugin({ handler }: any) { return { - onKeyDown(event: KeyboardEvent, editor: SlateEditor, next: Function) { + onKeyDown(event: any) { // Handle enter if (handler && event.key === 'Enter' && !event.shiftKey) { // Submit on Enter @@ -10,8 +8,7 @@ export default function RunnerPlugin({ handler }: any) { handler(event); return true; } - - return next(); + return undefined; }, }; } diff --git a/public/app/features/explore/slate-plugins/selection_shortcuts.ts b/public/app/features/explore/slate-plugins/selection_shortcuts.ts deleted file mode 100644 index d0849d34f04..00000000000 --- a/public/app/features/explore/slate-plugins/selection_shortcuts.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Plugin } from '@grafana/slate-react'; -import { Editor as CoreEditor } from 'slate'; - -import { isKeyHotkey } from 'is-hotkey'; - -const isSelectLeftHotkey = isKeyHotkey('shift+left'); -const isSelectRightHotkey = isKeyHotkey('shift+right'); -const isSelectUpHotkey = isKeyHotkey('shift+up'); -const isSelectDownHotkey = isKeyHotkey('shift+down'); -const isSelectLineHotkey = isKeyHotkey('mod+l'); - -const handleSelectVertical = (editor: CoreEditor, direction: 'up' | 'down') => { - const { focusBlock } = editor.value; - const adjacentBlock = - direction === 'up' - ? editor.value.document.getPreviousBlock(focusBlock.key) - : editor.value.document.getNextBlock(focusBlock.key); - - if (!adjacentBlock) { - return true; - } - const adjacentText = adjacentBlock.getFirstText(); - editor - .moveFocusTo(adjacentText.key, Math.min(editor.value.selection.anchor.offset, adjacentText.text.length)) - .focus(); - return true; -}; - -const handleSelectUp = (editor: CoreEditor) => handleSelectVertical(editor, 'up'); - -const handleSelectDown = (editor: CoreEditor) => handleSelectVertical(editor, 'down'); - -// Clears the rest of the line after the caret -export default function SelectionShortcutsPlugin(): Plugin { - return { - onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) { - if (isSelectLeftHotkey(event)) { - event.preventDefault(); - if (editor.value.selection.focus.offset > 0) { - editor.moveFocusBackward(1); - } - } else if (isSelectRightHotkey(event)) { - event.preventDefault(); - if (editor.value.selection.focus.offset < editor.value.startText.text.length) { - editor.moveFocusForward(1); - } - } else if (isSelectUpHotkey(event)) { - event.preventDefault(); - handleSelectUp(editor); - } else if (isSelectDownHotkey(event)) { - event.preventDefault(); - handleSelectDown(editor); - } else if (isSelectLineHotkey(event)) { - event.preventDefault(); - const { focusBlock, document } = editor.value; - - editor.moveAnchorToStartOfBlock(); - - const nextBlock = document.getNextBlock(focusBlock.key); - if (nextBlock) { - editor.moveFocusToStartOfNextBlock(); - } else { - editor.moveFocusToEndOfText(); - } - } else { - return next(); - } - - return true; - }, - }; -} diff --git a/public/app/features/explore/slate-plugins/suggestions.tsx b/public/app/features/explore/slate-plugins/suggestions.tsx deleted file mode 100644 index a3106ff5795..00000000000 --- a/public/app/features/explore/slate-plugins/suggestions.tsx +++ /dev/null @@ -1,313 +0,0 @@ -import React from 'react'; -import debounce from 'lodash/debounce'; -import sortBy from 'lodash/sortBy'; - -import { Editor as CoreEditor } from 'slate'; -import { Plugin as SlatePlugin } from '@grafana/slate-react'; -import { TypeaheadOutput, CompletionItem, CompletionItemGroup } from 'app/types'; - -import { QueryField, TypeaheadInput } from '../QueryField'; -import TOKEN_MARK from '@grafana/ui/src/slate-plugins/slate-prism/TOKEN_MARK'; -import { TypeaheadWithTheme, Typeahead } from '../Typeahead'; - -import { makeFragment } from '@grafana/ui'; - -export const TYPEAHEAD_DEBOUNCE = 100; - -export interface SuggestionsState { - groupedItems: CompletionItemGroup[]; - typeaheadPrefix: string; - typeaheadContext: string; - typeaheadText: string; -} - -let state: SuggestionsState = { - groupedItems: [], - typeaheadPrefix: '', - typeaheadContext: '', - typeaheadText: '', -}; - -export default function SuggestionsPlugin({ - onTypeahead, - cleanText, - onWillApplySuggestion, - syntax, - portalOrigin, - component, -}: { - onTypeahead: (typeahead: TypeaheadInput) => Promise; - cleanText?: (text: string) => string; - onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string; - syntax?: string; - portalOrigin: string; - component: QueryField; // Need to attach typeaheadRef here -}): SlatePlugin { - return { - onBlur: (event, editor, next) => { - state = { - ...state, - groupedItems: [], - }; - - return next(); - }, - - onClick: (event, editor, next) => { - state = { - ...state, - groupedItems: [], - }; - - return next(); - }, - - onKeyDown: (event: KeyboardEvent, editor, next) => { - const currentSuggestions = state.groupedItems; - - const hasSuggestions = currentSuggestions.length; - - switch (event.key) { - case 'Escape': { - if (hasSuggestions) { - event.preventDefault(); - - state = { - ...state, - groupedItems: [], - }; - - // Bogus edit to re-render editor - return editor.insertText(''); - } - - break; - } - - case 'ArrowDown': - case 'ArrowUp': - if (hasSuggestions) { - event.preventDefault(); - component.typeaheadRef.moveMenuIndex(event.key === 'ArrowDown' ? 1 : -1); - return; - } - - break; - - case 'Enter': - case 'Tab': { - if (hasSuggestions) { - event.preventDefault(); - - component.typeaheadRef.insertSuggestion(); - return handleTypeahead(event, editor, next, onTypeahead, cleanText); - } - - break; - } - - default: { - handleTypeahead(event, editor, next, onTypeahead, cleanText); - break; - } - } - - return next(); - }, - - commands: { - selectSuggestion: (editor: CoreEditor, suggestion: CompletionItem): CoreEditor => { - const suggestions = state.groupedItems; - if (!suggestions || !suggestions.length) { - return editor; - } - - // @ts-ignore - return editor.applyTypeahead(suggestion); - }, - - applyTypeahead: (editor: CoreEditor, suggestion: CompletionItem): CoreEditor => { - let suggestionText = suggestion.insertText || suggestion.label; - - const preserveSuffix = suggestion.kind === 'function'; - const move = suggestion.move || 0; - - const { typeaheadPrefix, typeaheadText, typeaheadContext } = state; - - if (onWillApplySuggestion) { - suggestionText = onWillApplySuggestion(suggestionText, { - groupedItems: state.groupedItems, - typeaheadContext, - typeaheadPrefix, - typeaheadText, - }); - } - - // Remove the current, incomplete text and replace it with the selected suggestion - const backward = suggestion.deleteBackwards || typeaheadPrefix.length; - const text = cleanText ? cleanText(typeaheadText) : typeaheadText; - const suffixLength = text.length - typeaheadPrefix.length; - const offset = typeaheadText.indexOf(typeaheadPrefix); - const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText); - const forward = midWord && !preserveSuffix ? suffixLength + offset : 0; - - // If new-lines, apply suggestion as block - if (suggestionText.match(/\n/)) { - const fragment = makeFragment(suggestionText); - return editor - .deleteBackward(backward) - .deleteForward(forward) - .insertFragment(fragment) - .focus(); - } - - state = { - ...state, - groupedItems: [], - }; - - return editor - .deleteBackward(backward) - .deleteForward(forward) - .insertText(suggestionText) - .moveForward(move) - .focus(); - }, - }, - - renderEditor: (props, editor, next) => { - if (editor.value.selection.isExpanded) { - return next(); - } - - const children = next(); - - return ( - <> - {children} - (component.typeaheadRef = el)} - origin={portalOrigin} - prefix={state.typeaheadPrefix} - isOpen={!!state.groupedItems.length} - groupedItems={state.groupedItems} - //@ts-ignore - onSelectSuggestion={editor.selectSuggestion} - /> - - ); - }, - }; -} - -const handleTypeahead = debounce( - async ( - event: Event, - editor: CoreEditor, - next: () => {}, - onTypeahead?: (typeahead: TypeaheadInput) => Promise, - cleanText?: (text: string) => string - ) => { - if (!onTypeahead) { - return next(); - } - - const { value } = editor; - const { selection } = value; - - // Get decorations associated with the current line - const parentBlock = value.document.getClosestBlock(value.focusBlock.key); - const myOffset = value.selection.start.offset - 1; - const decorations = parentBlock.getDecorations(editor as any); - - const filteredDecorations = decorations - .filter( - decoration => - decoration.start.offset <= myOffset && decoration.end.offset > myOffset && decoration.type === TOKEN_MARK - ) - .toArray(); - - const labelKeyDec = decorations - .filter( - decoration => - decoration.end.offset === myOffset && - decoration.type === TOKEN_MARK && - decoration.data.get('className').includes('label-key') - ) - .first(); - - const labelKey = labelKeyDec && value.focusText.text.slice(labelKeyDec.start.offset, labelKeyDec.end.offset); - - const wrapperClasses = filteredDecorations - .map(decoration => decoration.data.get('className')) - .join(' ') - .split(' ') - .filter(className => className.length); - - let text = value.focusText.text; - let prefix = text.slice(0, selection.focus.offset); - - if (filteredDecorations.length) { - text = value.focusText.text.slice(filteredDecorations[0].start.offset, filteredDecorations[0].end.offset); - prefix = value.focusText.text.slice(filteredDecorations[0].start.offset, selection.focus.offset); - } - - // Label values could have valid characters erased if `cleanText()` is - // blindly applied, which would undesirably interfere with suggestions - const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/); - if (labelValueMatch) { - prefix = labelValueMatch[1]; - } else if (cleanText) { - prefix = cleanText(prefix); - } - - const { suggestions, context } = await onTypeahead({ - prefix, - text, - value, - wrapperClasses, - labelKey, - }); - - const filteredSuggestions = suggestions - .map(group => { - if (!group.items) { - return group; - } - - if (prefix) { - // Filter groups based on prefix - if (!group.skipFilter) { - group.items = group.items.filter(c => (c.filterText || c.label).length >= prefix.length); - if (group.prefixMatch) { - group.items = group.items.filter(c => (c.filterText || c.label).startsWith(prefix)); - } else { - group.items = group.items.filter(c => (c.filterText || c.label).includes(prefix)); - } - } - - // Filter out the already typed value (prefix) unless it inserts custom text - group.items = group.items.filter(c => c.insertText || (c.filterText || c.label) !== prefix); - } - - if (!group.skipSort) { - group.items = sortBy(group.items, (item: CompletionItem) => item.sortText || item.label); - } - - return group; - }) - .filter(group => group.items && group.items.length); // Filter out empty groups - - state = { - ...state, - groupedItems: filteredSuggestions, - typeaheadPrefix: prefix, - typeaheadContext: context, - typeaheadText: text, - }; - - // Bogus edit to force re-render - return editor.insertText(''); - }, - TYPEAHEAD_DEBOUNCE -); diff --git a/public/app/features/explore/utils/typeahead.ts b/public/app/features/explore/utils/typeahead.ts index e501e2ab60a..7de817e4578 100644 --- a/public/app/features/explore/utils/typeahead.ts +++ b/public/app/features/explore/utils/typeahead.ts @@ -1,13 +1,14 @@ import { GrafanaTheme } from '@grafana/ui'; import { default as calculateSize } from 'calculate-size'; -import { CompletionItemGroup, CompletionItem, CompletionItemKind } from 'app/types'; +import { CompletionItemGroup, CompletionItem } from 'app/types'; +import { GROUP_TITLE_KIND } from '../TypeaheadItem'; export const flattenGroupItems = (groupedItems: CompletionItemGroup[]): CompletionItem[] => { return groupedItems.reduce((all, current) => { const titleItem: CompletionItem = { label: current.label, - kind: CompletionItemKind.GroupTitle, + kind: GROUP_TITLE_KIND, }; return all.concat(titleItem, current.items); }, []); @@ -55,7 +56,8 @@ export const calculateListWidth = (longestLabelWidth: number, theme: GrafanaThem export const calculateListHeight = (itemHeight: number, allItems: CompletionItem[]) => { const numberOfItemsToShow = Math.min(allItems.length, 10); const minHeight = 100; - const totalHeight = numberOfItemsToShow * itemHeight; + const itemsInView = allItems.slice(0, numberOfItemsToShow); + const totalHeight = itemsInView.length * itemHeight; const listHeight = Math.max(totalHeight, minHeight); return listHeight; diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 2b7af6e3225..1425eeb3226 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -10,7 +10,7 @@ import jquery from 'jquery'; import prismjs from 'prismjs'; import slate from 'slate'; // @ts-ignore -import slateReact from '@grafana/slate-react'; +import slateReact from 'slate-react'; // @ts-ignore import slatePlain from 'slate-plain-serializer'; import react from 'react'; @@ -91,7 +91,7 @@ exposeToPlugin('rxjs', { // Experimental modules exposeToPlugin('prismjs', prismjs); exposeToPlugin('slate', slate); -exposeToPlugin('@grafana/slate-react', slateReact); +exposeToPlugin('slate-react', slateReact); exposeToPlugin('slate-plain-serializer', slatePlain); exposeToPlugin('react', react); exposeToPlugin('react-dom', reactDom); diff --git a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx index 98c01ba1057..ab6ef40272c 100644 --- a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx @@ -1,7 +1,9 @@ import _ from 'lodash'; import React from 'react'; - -import { SlatePrism } from '@grafana/ui'; +// @ts-ignore +import PluginPrism from 'slate-prism'; +// @ts-ignore +import Prism from 'prismjs'; // dom also includes Element polyfills import QueryField from 'app/features/explore/QueryField'; @@ -22,7 +24,7 @@ class ElasticsearchQueryField extends React.PureComponent { super(props, context); this.plugins = [ - SlatePrism({ + PluginPrism({ onlyIn: (node: any) => node.type === 'code_block', getSyntax: (node: any) => 'lucene', }), diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 9e8c4a3205c..d3052691acc 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -1,13 +1,12 @@ import _ from 'lodash'; +// @ts-ignore import Plain from 'slate-plain-serializer'; import QueryField from './query_field'; import debounce from 'lodash/debounce'; import { DOMUtil } from '@grafana/ui'; -import { Editor as SlateEditor } from 'slate'; import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto/kusto'; -import { CompletionItem } from 'app/types'; // import '../sass/editor.base.scss'; const TYPEAHEAD_DELAY = 100; @@ -64,7 +63,7 @@ export default class KustoQueryField extends QueryField { this.fetchSchema(); } - onTypeahead = (force = false) => { + onTypeahead = (force?: boolean) => { const selection = window.getSelection(); if (selection.anchorNode) { const wrapperNode = selection.anchorNode.parentElement; @@ -197,15 +196,15 @@ export default class KustoQueryField extends QueryField { } }; - applyTypeahead = (editor: SlateEditor, suggestion: CompletionItem): SlateEditor => { + applyTypeahead(change: any, suggestion: { text: any; type: string; deleteBackwards: any }) { const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state; - let suggestionText = suggestion.label; + let suggestionText = suggestion.text || suggestion; const move = 0; // Modify suggestion based on context const nextChar = DOMUtil.getNextCharacter(); - if (suggestion.kind === 'function') { + if (suggestion.type === 'function') { if (!nextChar || nextChar !== '(') { suggestionText += '('; } @@ -229,13 +228,13 @@ export default class KustoQueryField extends QueryField { const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText); const forward = midWord ? suffixLength + offset : 0; - return editor + return change .deleteBackward(backward) .deleteForward(forward) .insertText(suggestionText) - .moveForward(move) + .move(move) .focus(); - }; + } // private _getFieldsSuggestions(): SuggestionGroup[] { // return [ diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 8f81da9a94a..42b2f1e858d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -7,13 +7,14 @@ import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; import Typeahead from './typeahead'; import { getKeybindingSrv, KeybindingSrv } from 'app/core/services/keybindingSrv'; -import { Block, Document, Text, Value, Editor as CoreEditor } from 'slate'; -import { Editor } from '@grafana/slate-react'; +import { Block, Document, Text, Value } from 'slate'; +// @ts-ignore +import { Editor } from 'slate-react'; +// @ts-ignore import Plain from 'slate-plain-serializer'; import ReactDOM from 'react-dom'; import React from 'react'; import _ from 'lodash'; -import { CompletionItem } from 'app/types'; function flattenSuggestions(s: any) { return s ? s.reduce((acc: any, g: any) => acc.concat(g.items), []) : []; @@ -97,7 +98,7 @@ class QueryField extends React.Component { this.updateMenu(); } - onChange = ({ value }: { value: Value }) => { + onChange = ({ value }: any) => { const changed = value.document !== this.state.value.document; this.setState({ value }, () => { if (changed) { @@ -123,15 +124,14 @@ class QueryField extends React.Component { } }; - onKeyDown = (event: Event, editor: CoreEditor, next: Function) => { + onKeyDown = (event: any, change: any) => { const { typeaheadIndex, suggestions } = this.state; - const keyboardEvent = event as KeyboardEvent; - switch (keyboardEvent.key) { + switch (event.key) { case 'Escape': { if (this.menuEl) { - keyboardEvent.preventDefault(); - keyboardEvent.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); this.resetTypeahead(); return true; } @@ -139,8 +139,8 @@ class QueryField extends React.Component { } case ' ': { - if (keyboardEvent.ctrlKey) { - keyboardEvent.preventDefault(); + if (event.ctrlKey) { + event.preventDefault(); this.onTypeahead(true); return true; } @@ -151,12 +151,18 @@ class QueryField extends React.Component { case 'Enter': { if (this.menuEl) { // Dont blur input - keyboardEvent.preventDefault(); + event.preventDefault(); if (!suggestions || suggestions.length === 0) { - return next(); + return undefined; } - this.applyTypeahead(); + // Get the currently selected suggestion + const flattenedSuggestions = flattenSuggestions(suggestions); + const selected = Math.abs(typeaheadIndex); + const selectedIndex = selected % flattenedSuggestions.length || 0; + const suggestion = flattenedSuggestions[selectedIndex]; + + this.applyTypeahead(change, suggestion); return true; } break; @@ -165,7 +171,7 @@ class QueryField extends React.Component { case 'ArrowDown': { if (this.menuEl) { // Select next suggestion - keyboardEvent.preventDefault(); + event.preventDefault(); this.setState({ typeaheadIndex: typeaheadIndex + 1 }); } break; @@ -174,7 +180,7 @@ class QueryField extends React.Component { case 'ArrowUp': { if (this.menuEl) { // Select previous suggestion - keyboardEvent.preventDefault(); + event.preventDefault(); this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) }); } break; @@ -185,16 +191,16 @@ class QueryField extends React.Component { break; } } - return next(); + return undefined; }; - onTypeahead = (change = false, item?: any): boolean | void => { - return change; + onTypeahead = (change?: boolean, item?: any) => { + return change || this.state.value.change(); }; - applyTypeahead = (editor?: CoreEditor, suggestion?: CompletionItem): { value: Value } => { - return { value: new Value() }; - }; + applyTypeahead(change?: boolean, suggestion?: any): { value: object } { + return { value: {} }; + } resetTypeahead = () => { this.setState({ @@ -239,8 +245,15 @@ class QueryField extends React.Component { return; } + // Get the currently selected suggestion + const flattenedSuggestions = flattenSuggestions(suggestions); + const suggestion: any = _.find( + flattenedSuggestions, + suggestion => suggestion.display === item || suggestion.text === item + ); + // Manually triggering change - const change = this.applyTypeahead(); + const change = this.applyTypeahead(this.state.value.change(), suggestion); this.onChange(change); }; diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index e90aff765be..de7f31ac7f8 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -1,7 +1,6 @@ import React, { FunctionComponent } from 'react'; import { LokiQueryFieldForm, LokiQueryFieldFormProps } from './LokiQueryFieldForm'; import { useLokiSyntax } from './useLokiSyntax'; -import LokiLanguageProvider from '../language_provider'; export const LokiQueryField: FunctionComponent = ({ datasource, @@ -9,7 +8,7 @@ export const LokiQueryField: FunctionComponent = ({ ...otherProps }) => { const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax( - datasource.languageProvider as LokiLanguageProvider, + datasource.languageProvider, datasourceStatus, otherProps.absoluteRange ); diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index f343bb5eab5..57b0b6987f0 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx @@ -2,24 +2,18 @@ import React from 'react'; // @ts-ignore import Cascader from 'rc-cascader'; - -import { SlatePrism } from '@grafana/ui'; - +// @ts-ignore +import PluginPrism from 'slate-prism'; // Components -import QueryField, { TypeaheadInput } from 'app/features/explore/QueryField'; +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; // Utils & Services // dom also includes Element polyfills import BracesPlugin from 'app/features/explore/slate-plugins/braces'; -import { Plugin, Node } from 'slate'; - // Types import { LokiQuery } from '../types'; -import { TypeaheadOutput } from 'app/types/explore'; +import { TypeaheadOutput, HistoryItem } from 'app/types/explore'; import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus, DOMUtil } from '@grafana/ui'; import { AbsoluteTimeRange } from '@grafana/data'; -import { Grammar } from 'prismjs'; -import LokiLanguageProvider, { LokiHistoryItem } from '../language_provider'; -import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions'; function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceStatus: DataSourceStatus) { if (datasourceStatus === DataSourceStatus.Disconnected) { @@ -34,7 +28,7 @@ function getChooserText(hasSyntax: boolean, hasLogLabels: boolean, datasourceSta return 'Log labels'; } -function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string { +function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string { // Modify suggestion based on context switch (typeaheadContext) { case 'context-labels': { @@ -69,17 +63,17 @@ export interface CascaderOption { } export interface LokiQueryFieldFormProps extends ExploreQueryFieldProps, LokiQuery> { - history: LokiHistoryItem[]; - syntax: Grammar; + history: HistoryItem[]; + syntax: any; logLabelOptions: any[]; - syntaxLoaded: boolean; + syntaxLoaded: any; absoluteRange: AbsoluteTimeRange; onLoadOptions: (selectedOptions: CascaderOption[]) => void; onLabelsRefresh?: () => void; } export class LokiQueryFieldForm extends React.PureComponent { - plugins: Plugin[]; + plugins: any[]; modifiedSearch: string; modifiedQuery: string; @@ -88,9 +82,9 @@ export class LokiQueryFieldForm extends React.PureComponent node.object === 'block' && node.type === 'code_block', - getSyntax: (node: Node) => 'promql', + PluginPrism({ + onlyIn: (node: any) => node.type === 'code_block', + getSyntax: (node: any) => 'promql', }), ]; } @@ -121,23 +115,27 @@ export class LokiQueryFieldForm extends React.PureComponent => { + onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => { const { datasource } = this.props; - if (!datasource.languageProvider) { return { suggestions: [] }; } - const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider; const { history, absoluteRange } = this.props; - const { prefix, text, value, wrapperClasses, labelKey } = typeahead; + const { prefix, text, value, wrapperNode } = typeahead; + + // Get DOM-dependent context + const wrapperClasses = Array.from(wrapperNode.classList); + const labelKeyNode = DOMUtil.getPreviousCousin(wrapperNode, '.attr-name'); + const labelKey = labelKeyNode && labelKeyNode.textContent; + const nextChar = DOMUtil.getNextCharacter(); - const result = await lokiLanguageProvider.provideCompletionItems( + const result = datasource.languageProvider.provideCompletionItems( { text, value, prefix, wrapperClasses, labelKey }, { history, absoluteRange } ); - //console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context); + console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context); return result; }; @@ -153,8 +151,7 @@ export class LokiQueryFieldForm extends React.PureComponent 0; const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus); const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected; diff --git a/public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts b/public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts index 07c98cc476c..62de5c156ad 100644 --- a/public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts +++ b/public/app/plugins/datasource/loki/components/useLokiSyntax.test.ts @@ -3,7 +3,6 @@ import { DataSourceStatus } from '@grafana/ui/src/types/datasource'; import { AbsoluteTimeRange } from '@grafana/data'; import LanguageProvider from 'app/plugins/datasource/loki/language_provider'; - import { useLokiSyntax } from './useLokiSyntax'; import { CascaderOption } from 'app/plugins/datasource/loki/components/LokiQueryFieldForm'; import { makeMockLokiDatasource } from '../mocks'; diff --git a/public/app/plugins/datasource/loki/components/useLokiSyntax.ts b/public/app/plugins/datasource/loki/components/useLokiSyntax.ts index f4ae3652e4d..7faa5a6fb24 100644 --- a/public/app/plugins/datasource/loki/components/useLokiSyntax.ts +++ b/public/app/plugins/datasource/loki/components/useLokiSyntax.ts @@ -1,4 +1,5 @@ import { useState, useEffect } from 'react'; +// @ts-ignore import Prism from 'prismjs'; import { DataSourceStatus } from '@grafana/ui/src/types/datasource'; import { AbsoluteTimeRange } from '@grafana/data'; diff --git a/public/app/plugins/datasource/loki/language_provider.test.ts b/public/app/plugins/datasource/loki/language_provider.test.ts index 0b8caa16f0a..4f0ac9324aa 100644 --- a/public/app/plugins/datasource/loki/language_provider.test.ts +++ b/public/app/plugins/datasource/loki/language_provider.test.ts @@ -1,14 +1,13 @@ +// @ts-ignore import Plain from 'slate-plain-serializer'; -import { Editor as SlateEditor } from 'slate'; import LanguageProvider, { LABEL_REFRESH_INTERVAL, LokiHistoryItem, rangeToParams } from './language_provider'; import { AbsoluteTimeRange } from '@grafana/data'; import { advanceTo, clear, advanceBy } from 'jest-date-mock'; import { beforeEach } from 'test/lib/common'; - +import { DataSourceApi } from '@grafana/ui'; import { TypeaheadInput } from '../../../types'; import { makeMockLokiDatasource } from './mocks'; -import LokiDatasource from './datasource'; describe('Language completion provider', () => { const datasource = makeMockLokiDatasource({}); @@ -19,16 +18,16 @@ describe('Language completion provider', () => { }; describe('empty query suggestions', () => { - it('returns no suggestions on empty context', async () => { + it('returns no suggestions on empty context', () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize(''); - const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); - + expect(result.refresher).toBeUndefined(); expect(result.suggestions.length).toEqual(0); }); - it('returns default suggestions with history on empty context when history was provided', async () => { + it('returns default suggestions with history on empty context when history was provided', () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize(''); const history: LokiHistoryItem[] = [ @@ -37,12 +36,12 @@ describe('Language completion provider', () => { ts: 1, }, ]; - const result = await instance.provideCompletionItems( + const result = instance.provideCompletionItems( { text: '', prefix: '', value, wrapperClasses: [] }, { history, absoluteRange: rangeMock } ); expect(result.context).toBeUndefined(); - + expect(result.refresher).toBeUndefined(); expect(result.suggestions).toMatchObject([ { label: 'History', @@ -55,7 +54,7 @@ describe('Language completion provider', () => { ]); }); - it('returns no suggestions within regexp', async () => { + it('returns no suggestions within regexp', () => { const instance = new LanguageProvider(datasource); const input = createTypeaheadInput('{} ()', '', undefined, 4, []); const history: LokiHistoryItem[] = [ @@ -64,28 +63,18 @@ describe('Language completion provider', () => { ts: 1, }, ]; - const result = await instance.provideCompletionItems(input, { history }); + const result = instance.provideCompletionItems(input, { history }); expect(result.context).toBeUndefined(); - + expect(result.refresher).toBeUndefined(); expect(result.suggestions.length).toEqual(0); }); }); describe('label suggestions', () => { - it('returns default label suggestions on label context', async () => { + it('returns default label suggestions on label context', () => { const instance = new LanguageProvider(datasource); - const value = Plain.deserialize('{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(1).value; - const result = await instance.provideCompletionItems( - { - text: '', - prefix: '', - wrapperClasses: ['context-labels'], - value: valueWithSelection, - }, - { absoluteRange: rangeMock } - ); + const input = createTypeaheadInput('{}', ''); + const result = instance.provideCompletionItems(input, { absoluteRange: rangeMock }); expect(result.context).toBe('context-labels'); expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]); }); @@ -94,7 +83,7 @@ describe('Language completion provider', () => { const datasource = makeMockLokiDatasource({ label1: [], label2: [] }); const provider = await getLanguageProvider(datasource); const input = createTypeaheadInput('{}', ''); - const result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock }); + const result = provider.provideCompletionItems(input, { absoluteRange: rangeMock }); expect(result.context).toBe('context-labels'); expect(result.suggestions).toEqual([{ items: [{ label: 'label1' }, { label: 'label2' }], label: 'Labels' }]); }); @@ -103,9 +92,11 @@ describe('Language completion provider', () => { const datasource = makeMockLokiDatasource({ label1: ['label1_val1', 'label1_val2'], label2: [] }); const provider = await getLanguageProvider(datasource); const input = createTypeaheadInput('{label1=}', '=', 'label1'); - let result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock }); - - result = await provider.provideCompletionItems(input, { absoluteRange: rangeMock }); + let result = provider.provideCompletionItems(input, { absoluteRange: rangeMock }); + // The values for label are loaded adhoc and there is a promise returned that we have to wait for + expect(result.refresher).toBeDefined(); + await result.refresher; + result = provider.provideCompletionItems(input, { absoluteRange: rangeMock }); expect(result.context).toBe('context-label-values'); expect(result.suggestions).toEqual([ { items: [{ label: 'label1_val1' }, { label: 'label1_val2' }], label: 'Label values for "label1"' }, @@ -210,7 +201,7 @@ describe('Labels refresh', () => { }); }); -async function getLanguageProvider(datasource: LokiDatasource) { +async function getLanguageProvider(datasource: DataSourceApi) { const instance = new LanguageProvider(datasource); instance.initialRange = { from: Date.now() - 10000, @@ -233,8 +224,10 @@ function createTypeaheadInput( wrapperClasses?: string[] ): TypeaheadInput { const deserialized = Plain.deserialize(value); - const range = deserialized.selection.setAnchor(deserialized.selection.anchor.setOffset(anchorOffset || 1)); - const valueWithSelection = deserialized.setSelection(range); + const range = deserialized.selection.merge({ + anchorOffset: anchorOffset || 1, + }); + const valueWithSelection = deserialized.change().select(range).value; return { text, prefix: '', diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts index ac5f877f8d4..44ba031255f 100644 --- a/public/app/plugins/datasource/loki/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -6,12 +6,18 @@ import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasour import syntax from './syntax'; // Types -import { CompletionItem, LanguageProvider, TypeaheadInput, TypeaheadOutput, HistoryItem } from 'app/types/explore'; +import { + CompletionItem, + CompletionItemGroup, + LanguageProvider, + TypeaheadInput, + TypeaheadOutput, + HistoryItem, +} from 'app/types/explore'; import { LokiQuery } from './types'; import { dateTime, AbsoluteTimeRange } from '@grafana/data'; import { PromQuery } from '../prometheus/types'; - -import LokiDatasource from './datasource'; +import { DataSourceApi } from '@grafana/ui'; const DEFAULT_KEYS = ['job', 'namespace']; const EMPTY_SELECTOR = '{}'; @@ -53,9 +59,8 @@ export default class LokiLanguageProvider extends LanguageProvider { logLabelFetchTs?: number; started: boolean; initialRange: AbsoluteTimeRange; - datasource: LokiDatasource; - constructor(datasource: LokiDatasource, initialValues?: any) { + constructor(datasource: DataSourceApi, initialValues?: any) { super(); this.datasource = datasource; @@ -64,7 +69,6 @@ export default class LokiLanguageProvider extends LanguageProvider { Object.assign(this, initialValues); } - // Strip syntax chars cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); @@ -107,14 +111,14 @@ export default class LokiLanguageProvider extends LanguageProvider { * @param context.absoluteRange Required in case we are doing getLabelCompletionItems * @param context.history Optional used only in getEmptyCompletionItems */ - async provideCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): Promise { + provideCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): TypeaheadOutput { const { wrapperClasses, value } = input; // Local text properties const empty = value.document.text.length === 0; // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-labels')) { // Suggestions for {|} and {foo=|} - return await this.getLabelCompletionItems(input, context); + return this.getLabelCompletionItems(input, context); } else if (empty) { return this.getEmptyCompletionItems(context || {}); } @@ -126,7 +130,7 @@ export default class LokiLanguageProvider extends LanguageProvider { getEmptyCompletionItems(context: any): TypeaheadOutput { const { history } = context; - const suggestions = []; + const suggestions: CompletionItemGroup[] = []; if (history && history.length > 0) { const historyItems = _.chain(history) @@ -149,14 +153,15 @@ export default class LokiLanguageProvider extends LanguageProvider { return { suggestions }; } - async getLabelCompletionItems( + getLabelCompletionItems( { text, wrapperClasses, labelKey, value }: TypeaheadInput, { absoluteRange }: any - ): Promise { + ): TypeaheadOutput { let context: string; - const suggestions = []; + let refresher: Promise = null; + const suggestions: CompletionItemGroup[] = []; const line = value.anchorBlock.getText(); - const cursorOffset: number = value.selection.anchor.offset; + const cursorOffset: number = value.anchorOffset; // Use EMPTY_SELECTOR until series API is implemented for facetting const selector = EMPTY_SELECTOR; @@ -166,20 +171,19 @@ export default class LokiLanguageProvider extends LanguageProvider { } catch {} const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; - if ((text && text.match(/^!?=~?/)) || wrapperClasses.includes('attr-value')) { + if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) { // Label values if (labelKey && this.labelValues[selector]) { - let labelValues = this.labelValues[selector][labelKey]; - if (!labelValues) { - await this.fetchLabelValues(labelKey, absoluteRange); - labelValues = this.labelValues[selector][labelKey]; + const labelValues = this.labelValues[selector][labelKey]; + if (labelValues) { + context = 'context-label-values'; + suggestions.push({ + label: `Label values for "${labelKey}"`, + items: labelValues.map(wrapLabel), + }); + } else { + refresher = this.fetchLabelValues(labelKey, absoluteRange); } - - context = 'context-label-values'; - suggestions.push({ - label: `Label values for "${labelKey}"`, - items: labelValues.map(wrapLabel), - }); } } else { // Label keys @@ -193,7 +197,7 @@ export default class LokiLanguageProvider extends LanguageProvider { } } - return { context, suggestions }; + return { context, refresher, suggestions }; } async importQueries(queries: LokiQuery[], datasourceType: string): Promise { diff --git a/public/app/plugins/datasource/loki/mocks.ts b/public/app/plugins/datasource/loki/mocks.ts index 7e91c51c105..49c2de7dcc0 100644 --- a/public/app/plugins/datasource/loki/mocks.ts +++ b/public/app/plugins/datasource/loki/mocks.ts @@ -1,6 +1,6 @@ -import LokiDatasource from './datasource'; +import { DataSourceApi } from '@grafana/ui'; -export function makeMockLokiDatasource(labelsAndValues: { [label: string]: string[] }): LokiDatasource { +export function makeMockLokiDatasource(labelsAndValues: { [label: string]: string[] }): DataSourceApi { const labels = Object.keys(labelsAndValues); return { metadataRequest: (url: string) => { diff --git a/public/app/plugins/datasource/loki/syntax.ts b/public/app/plugins/datasource/loki/syntax.ts index 2e83723a815..0748b4e5ffd 100644 --- a/public/app/plugins/datasource/loki/syntax.ts +++ b/public/app/plugins/datasource/loki/syntax.ts @@ -1,8 +1,6 @@ -import { Grammar } from 'prismjs'; - /* tslint:disable max-line-length */ -const tokenizer: Grammar = { +const tokenizer = { comment: { pattern: /(^|[^\n])#.*/, lookbehind: true, diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 963a2d79a30..99eef38bde3 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -2,22 +2,20 @@ import _ from 'lodash'; import React from 'react'; // @ts-ignore import Cascader from 'rc-cascader'; - -import { SlatePrism } from '@grafana/ui'; - +// @ts-ignore +import PluginPrism from 'slate-prism'; +// @ts-ignore import Prism from 'prismjs'; import { TypeaheadOutput, HistoryItem } from 'app/types/explore'; // dom also includes Element polyfills import BracesPlugin from 'app/features/explore/slate-plugins/braces'; -import QueryField, { TypeaheadInput } from 'app/features/explore/QueryField'; +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; import { PromQuery, PromContext, PromOptions } from '../types'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; import { ExploreQueryFieldProps, DataSourceStatus, QueryHint, DOMUtil } from '@grafana/ui'; import { isDataFrame, toLegacyResponseData } from '@grafana/data'; import { PrometheusDatasource } from '../datasource'; -import PromQlLanguageProvider from '../language_provider'; -import { SuggestionsState } from 'app/features/explore/slate-plugins/suggestions'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -69,7 +67,7 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad return [...options, ...metricsOptions]; } -export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: SuggestionsState): string { +export function willApplySuggestion(suggestion: string, { typeaheadContext, typeaheadText }: QueryFieldState): string { // Modify suggestion based on context switch (typeaheadContext) { case 'context-labels': { @@ -104,7 +102,7 @@ interface CascaderOption { } interface PromQueryFieldProps extends ExploreQueryFieldProps { - history: Array>; + history: HistoryItem[]; } interface PromQueryFieldState { @@ -115,7 +113,7 @@ interface PromQueryFieldState { class PromQueryField extends React.PureComponent { plugins: any[]; - languageProvider: PromQlLanguageProvider; + languageProvider: any; languageProviderInitializationPromise: CancelablePromise; constructor(props: PromQueryFieldProps, context: React.Context) { @@ -127,7 +125,7 @@ class PromQueryField extends React.PureComponent node.type === 'code_block', getSyntax: (node: any) => 'promql', }), @@ -254,7 +252,7 @@ class PromQueryField extends React.PureComponent => { + onTypeahead = (typeahead: TypeaheadInput): TypeaheadOutput => { if (!this.languageProvider) { return { suggestions: [] }; } const { history } = this.props; - const { prefix, text, value, wrapperClasses, labelKey } = typeahead; + const { prefix, text, value, wrapperNode } = typeahead; + + // Get DOM-dependent context + const wrapperClasses = Array.from(wrapperNode.classList); + const labelKeyNode = DOMUtil.getPreviousCousin(wrapperNode, '.attr-name'); + const labelKey = labelKeyNode && labelKeyNode.textContent; + const nextChar = DOMUtil.getNextCharacter(); - const result = await this.languageProvider.provideCompletionItems( + const result = this.languageProvider.provideCompletionItems( { text, value, prefix, wrapperClasses, labelKey }, { history } ); - // console.log('handleTypeahead', wrapperClasses, text, prefix, labelKey, result.context); + console.log('handleTypeahead', wrapperClasses, text, prefix, nextChar, labelKey, result.context); return result; }; diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 018b32e4981..86cdb57f02a 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -1,28 +1,23 @@ import _ from 'lodash'; -import { dateTime } from '@grafana/data'; - import { CompletionItem, CompletionItemGroup, LanguageProvider, TypeaheadInput, TypeaheadOutput, - HistoryItem, } from 'app/types/explore'; import { parseSelector, processLabels, processHistogramLabels } from './language_utils'; import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql'; - -import { PrometheusDatasource } from './datasource'; -import { PromQuery } from './types'; +import { dateTime } from '@grafana/data'; const DEFAULT_KEYS = ['job', 'instance']; const EMPTY_SELECTOR = '{}'; const HISTORY_ITEM_COUNT = 5; const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h -const wrapLabel = (label: string): CompletionItem => ({ label }); +const wrapLabel = (label: string) => ({ label }); const setFunctionKind = (suggestion: CompletionItem): CompletionItem => { suggestion.kind = 'function'; @@ -35,12 +30,10 @@ export function addHistoryMetadata(item: CompletionItem, history: any[]): Comple const count = historyForItem.length; const recent = historyForItem[0]; let hint = `Queried ${count} times in the last 24h.`; - if (recent) { const lastQueried = dateTime(recent.ts).fromNow(); hint = `${hint} Last queried ${lastQueried}.`; } - return { ...item, documentation: hint, @@ -54,9 +47,8 @@ export default class PromQlLanguageProvider extends LanguageProvider { labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] metrics?: string[]; startTask: Promise; - datasource: PrometheusDatasource; - constructor(datasource: PrometheusDatasource, initialValues?: any) { + constructor(datasource: any, initialValues?: any) { super(); this.datasource = datasource; @@ -68,11 +60,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { Object.assign(this, initialValues); } - // Strip syntax chars cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); - get syntax() { + getSyntax() { return PromqlSyntax; } @@ -115,46 +106,39 @@ export default class PromQlLanguageProvider extends LanguageProvider { } }; - provideCompletionItems = async ( - { prefix, text, value, labelKey, wrapperClasses }: TypeaheadInput, - context: { history: Array> } = { history: [] } - ): Promise => { + // Keep this DOM-free for testing + provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput { // Local text properties const empty = value.document.text.length === 0; - const selectedLines = value.document.getTextsAtRange(value.selection); - const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null; - - const nextCharacter = currentLine ? currentLine[value.selection.anchor.offset] : null; + const selectedLines = value.document.getTextsAtRangeAsArray(value.selection); + const currentLine = selectedLines.length === 1 ? selectedLines[0] : null; + const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null; // Syntax spans have 3 classes by default. More indicate a recognized token const tokenRecognized = wrapperClasses.length > 3; // Non-empty prefix, but not inside known token const prefixUnrecognized = prefix && !tokenRecognized; - // Prevent suggestions in `function(|suffix)` const noSuffix = !nextCharacter || nextCharacter === ')'; - - // Empty prefix is safe if it does not immediately follow a complete expression and has no text after it + // Empty prefix is safe if it does not immediately folllow a complete expression and has no text after it const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix; - // About to type next operand if preceded by binary operator - const operatorsPattern = /[+\-*/^%]/; - const isNextOperand = text.match(operatorsPattern); + const isNextOperand = text.match(/[+\-*/^%]/); // Determine candidates by CSS context - if (wrapperClasses.includes('context-range')) { + if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] return this.getRangeCompletionItems(); - } else if (wrapperClasses.includes('context-labels')) { + } else if (_.includes(wrapperClasses, 'context-labels')) { // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} - return this.getLabelCompletionItems({ prefix, text, value, labelKey, wrapperClasses }); - } else if (wrapperClasses.includes('context-aggregation')) { + return this.getLabelCompletionItems.apply(this, arguments); + } else if (_.includes(wrapperClasses, 'context-aggregation')) { // Suggestions for sum(metric) by (|) - return this.getAggregationCompletionItems({ prefix, text, value, labelKey, wrapperClasses }); + return this.getAggregationCompletionItems.apply(this, arguments); } else if (empty) { // Suggestions for empty query field - return this.getEmptyCompletionItems(context); - } else if ((prefixUnrecognized && noSuffix) || safeEmptyPrefix || isNextOperand) { + return this.getEmptyCompletionItems(context || {}); + } else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) { // Show term suggestions in a couple of scenarios return this.getTermCompletionItems(); } @@ -162,20 +146,20 @@ export default class PromQlLanguageProvider extends LanguageProvider { return { suggestions: [], }; - }; + } - getEmptyCompletionItems = (context: { history: Array> }): TypeaheadOutput => { + getEmptyCompletionItems(context: any): TypeaheadOutput { const { history } = context; - const suggestions = []; + let suggestions: CompletionItemGroup[] = []; - if (history && history.length) { + if (history && history.length > 0) { const historyItems = _.chain(history) - .map(h => h.query.expr) + .map((h: any) => h.query.expr) .filter() .uniq() .take(HISTORY_ITEM_COUNT) .map(wrapLabel) - .map(item => addHistoryMetadata(item, history)) + .map((item: CompletionItem) => addHistoryMetadata(item, history)) .value(); suggestions.push({ @@ -187,14 +171,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { } const termCompletionItems = this.getTermCompletionItems(); - suggestions.push(...termCompletionItems.suggestions); + suggestions = [...suggestions, ...termCompletionItems.suggestions]; return { suggestions }; - }; + } - getTermCompletionItems = (): TypeaheadOutput => { + getTermCompletionItems(): TypeaheadOutput { const { metrics } = this; - const suggestions = []; + const suggestions: CompletionItemGroup[] = []; suggestions.push({ prefixMatch: true, @@ -202,15 +186,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { items: FUNCTIONS.map(setFunctionKind), }); - if (metrics && metrics.length) { + if (metrics && metrics.length > 0) { suggestions.push({ label: 'Metrics', items: metrics.map(wrapLabel), }); } - return { suggestions }; - }; + } getRangeCompletionItems(): TypeaheadOutput { return { @@ -236,21 +219,21 @@ export default class PromQlLanguageProvider extends LanguageProvider { ); } - getAggregationCompletionItems = ({ value }: TypeaheadInput): TypeaheadOutput => { + getAggregationCompletionItems({ value }: TypeaheadInput): TypeaheadOutput { const refresher: Promise = null; const suggestions: CompletionItemGroup[] = []; // Stitch all query lines together to support multi-line queries let queryOffset; - const queryText = value.document.getBlocks().reduce((text: string, block) => { + const queryText = value.document.getBlocks().reduce((text: string, block: any) => { const blockText = block.getText(); if (value.anchorBlock.key === block.key) { // Newline characters are not accounted for but this is irrelevant // for the purpose of extracting the selector string - queryOffset = value.selection.anchor.offset + text.length; + queryOffset = value.anchorOffset + text.length; } - - return text + blockText; + text += blockText; + return text; }, ''); // Try search for selector part on the left-hand side, such as `sum (m) by (l)` @@ -276,10 +259,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { return result; } + let selectorString = queryText.slice(openParensSelectorIndex + 1, closeParensSelectorIndex); + // Range vector syntax not accounted for by subsequent parse so discard it if present - const selectorString = queryText - .slice(openParensSelectorIndex + 1, closeParensSelectorIndex) - .replace(/\[[^\]]+\]$/, ''); + selectorString = selectorString.replace(/\[[^\]]+\]$/, ''); const selector = parseSelector(selectorString, selectorString.length - 2).selector; @@ -291,16 +274,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { } return result; - }; + } - getLabelCompletionItems = async ({ - text, - wrapperClasses, - labelKey, - value, - }: TypeaheadInput): Promise => { + getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput { + let context: string; + let refresher: Promise = null; + const suggestions: CompletionItemGroup[] = []; const line = value.anchorBlock.getText(); - const cursorOffset = value.selection.anchor.offset; + const cursorOffset: number = value.anchorOffset; // Get normalized selector let selector; @@ -311,23 +292,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { } catch { selector = EMPTY_SELECTOR; } - - const containsMetric = selector.includes('__name__='); + const containsMetric = selector.indexOf('__name__=') > -1; const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; - // Query labels for selector - if (selector && (!this.labelValues[selector] || this.timeRangeChanged())) { - if (selector === EMPTY_SELECTOR) { - // Query label values for default labels - await Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key))); - } else { - await this.fetchSeriesLabels(selector, !containsMetric); - } - } - - const suggestions = []; - let context: string; - if ((text && text.match(/^!?=~?/)) || wrapperClasses.includes('attr-value')) { + if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) { // Label values if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) { const labelValues = this.labelValues[selector][labelKey]; @@ -340,20 +308,27 @@ export default class PromQlLanguageProvider extends LanguageProvider { } else { // Label keys const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS); - if (labelKeys) { const possibleKeys = _.difference(labelKeys, existingKeys); - if (possibleKeys.length) { + if (possibleKeys.length > 0) { context = 'context-labels'; - const newItems = possibleKeys.map(key => ({ label: key })); - const newSuggestion: CompletionItemGroup = { label: `Labels`, items: newItems }; - suggestions.push(newSuggestion); + suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) }); } } } - return { context, suggestions }; - }; + // Query labels for selector + if (selector && (!this.labelValues[selector] || this.timeRangeChanged())) { + if (selector === EMPTY_SELECTOR) { + // Query label values for default labels + refresher = Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key))); + } else { + refresher = this.fetchSeriesLabels(selector, !containsMetric); + } + } + + return { context, refresher, suggestions }; + } fetchLabelValues = async (key: string) => { try { diff --git a/public/app/plugins/datasource/prometheus/language_utils.ts b/public/app/plugins/datasource/prometheus/language_utils.ts index 535f86fc8bc..f10d9eed422 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.ts @@ -16,13 +16,13 @@ export const processHistogramLabels = (labels: string[]) => { return { values: { __name__: result } }; }; -export function processLabels(labels: Array<{ [key: string]: string }>, withName = false) { +export function processLabels(labels: any, withName = false) { const values: { [key: string]: string[] } = {}; - labels.forEach(l => { + labels.forEach((l: any) => { const { __name__, ...rest } = l; if (withName) { values['__name__'] = values['__name__'] || []; - if (!values['__name__'].includes(__name__)) { + if (values['__name__'].indexOf(__name__) === -1) { values['__name__'].push(__name__); } } @@ -31,7 +31,7 @@ export function processLabels(labels: Array<{ [key: string]: string }>, withName if (!values[key]) { values[key] = []; } - if (!values[key].includes(rest[key])) { + if (values[key].indexOf(rest[key]) === -1) { values[key].push(rest[key]); } }); diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 9171201fc37..fe1679eb94b 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -1,22 +1,21 @@ +// @ts-ignore import Plain from 'slate-plain-serializer'; -import { Editor as SlateEditor } from 'slate'; + import LanguageProvider from '../language_provider'; -import { PrometheusDatasource } from '../datasource'; -import { HistoryItem } from 'app/types'; -import { PromQuery } from '../types'; describe('Language completion provider', () => { - const datasource: PrometheusDatasource = ({ + const datasource = { metadataRequest: () => ({ data: { data: [] as any[] } }), getTimeRange: () => ({ start: 0, end: 1 }), - } as any) as PrometheusDatasource; + }; describe('empty query suggestions', () => { - it('returns default suggestions on empty context', async () => { + it('returns default suggestions on emtpty context', () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize(''); - const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); expect(result.suggestions).toMatchObject([ { label: 'Functions', @@ -24,11 +23,12 @@ describe('Language completion provider', () => { ]); }); - it('returns default suggestions with metrics on empty context when metrics were provided', async () => { + it('returns default suggestions with metrics on emtpty context when metrics were provided', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); const value = Plain.deserialize(''); - const result = await instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); expect(result.suggestions).toMatchObject([ { label: 'Functions', @@ -39,21 +39,17 @@ describe('Language completion provider', () => { ]); }); - it('returns default suggestions with history on empty context when history was provided', async () => { + it('returns default suggestions with history on emtpty context when history was provided', () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize(''); - const history: Array> = [ + const history = [ { - ts: 0, query: { refId: '1', expr: 'metric' }, }, ]; - const result = await instance.provideCompletionItems( - { text: '', prefix: '', value, wrapperClasses: [] }, - { history } - ); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history }); expect(result.context).toBeUndefined(); - + expect(result.refresher).toBeUndefined(); expect(result.suggestions).toMatchObject([ { label: 'History', @@ -71,16 +67,17 @@ describe('Language completion provider', () => { }); describe('range suggestions', () => { - it('returns range suggestions in range context', async () => { + it('returns range suggestions in range context', () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize('1'); - const result = await instance.provideCompletionItems({ + const result = instance.provideCompletionItems({ text: '1', prefix: '1', value, wrapperClasses: ['context-range'], }); expect(result.context).toBe('context-range'); + expect(result.refresher).toBeUndefined(); expect(result.suggestions).toMatchObject([ { items: [ @@ -99,12 +96,12 @@ describe('Language completion provider', () => { }); describe('metric suggestions', () => { - it('returns metrics and function suggestions in an unknown context', async () => { + it('returns metrics and function suggestions in an unknown context', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - let value = Plain.deserialize('a'); - value = value.setSelection({ anchor: { offset: 1 }, focus: { offset: 1 } }); - const result = await instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] }); + const value = Plain.deserialize('a'); + const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); expect(result.suggestions).toMatchObject([ { label: 'Functions', @@ -115,11 +112,12 @@ describe('Language completion provider', () => { ]); }); - it('returns metrics and function suggestions after a binary operator', async () => { + it('returns metrics and function suggestions after a binary operator', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); const value = Plain.deserialize('*'); - const result = await instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] }); + const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); expect(result.suggestions).toMatchObject([ { label: 'Functions', @@ -130,30 +128,34 @@ describe('Language completion provider', () => { ]); }); - it('returns no suggestions at the beginning of a non-empty function', async () => { + it('returns no suggestions at the beginning of a non-empty function', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); const value = Plain.deserialize('sum(up)'); - const ed = new SlateEditor({ value }); - - const valueWithSelection = ed.moveForward(4).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 4, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', value: valueWithSelection, wrapperClasses: [], }); expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); expect(result.suggestions.length).toEqual(0); }); }); describe('label suggestions', () => { - it('returns default label suggestions on label context and no metric', async () => { + it('returns default label suggestions on label context and no metric', () => { const instance = new LanguageProvider(datasource); const value = Plain.deserialize('{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(1).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 1, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-labels'], @@ -163,16 +165,14 @@ describe('Language completion provider', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]); }); - it('returns label suggestions on label context and metric', async () => { - const datasources: PrometheusDatasource = ({ - metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }), - getTimeRange: () => ({ start: 0, end: 1 }), - } as any) as PrometheusDatasource; - const instance = new LanguageProvider(datasources, { labelKeys: { '{__name__="metric"}': ['bar'] } }); + it('returns label suggestions on label context and metric', () => { + const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } }); const value = Plain.deserialize('metric{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(7).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 7, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-labels'], @@ -182,32 +182,16 @@ describe('Language completion provider', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); - it('returns label suggestions on label context but leaves out labels that already exist', async () => { - const datasources: PrometheusDatasource = ({ - metadataRequest: () => ({ - data: { - data: [ - { - __name__: 'metric', - bar: 'asdasd', - job1: 'dsadsads', - job2: 'fsfsdfds', - job3: 'dsadsad', - }, - ], - }, - }), - getTimeRange: () => ({ start: 0, end: 1 }), - } as any) as PrometheusDatasource; - const instance = new LanguageProvider(datasources, { - labelKeys: { - '{job1="foo",job2!="foo",job3=~"foo",__name__="metric"}': ['bar', 'job1', 'job2', 'job3', '__name__'], - }, + it('returns label suggestions on label context but leaves out labels that already exist', () => { + const instance = new LanguageProvider(datasource, { + labelKeys: { '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] }, }); - const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(54).value; - const result = await instance.provideCompletionItems({ + const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}'); + const range = value.selection.merge({ + anchorOffset: 36, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-labels'], @@ -217,15 +201,15 @@ describe('Language completion provider', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); - it('returns label value suggestions inside a label value context after a negated matching operator', async () => { + it('returns label value suggestions inside a label value context after a negated matching operator', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{}': ['label'] }, labelValues: { '{}': { label: ['a', 'b', 'c'] } }, }); const value = Plain.deserialize('{label!=}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(8).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ anchorOffset: 8 }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '!=', prefix: '', wrapperClasses: ['context-labels'], @@ -241,30 +225,35 @@ describe('Language completion provider', () => { ]); }); - it('returns a refresher on label context and unavailable metric', async () => { + it('returns a refresher on label context and unavailable metric', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="foo"}': ['bar'] } }); const value = Plain.deserialize('metric{}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(7).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 7, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-labels'], value: valueWithSelection, }); expect(result.context).toBeUndefined(); + expect(result.refresher).toBeInstanceOf(Promise); expect(result.suggestions).toEqual([]); }); - it('returns label values on label context when given a metric and a label key', async () => { + it('returns label values on label context when given a metric and a label key', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] }, labelValues: { '{__name__="metric"}': { bar: ['baz'] } }, }); const value = Plain.deserialize('metric{bar=ba}'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(13).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 13, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '=ba', prefix: 'ba', wrapperClasses: ['context-labels'], @@ -275,12 +264,14 @@ describe('Language completion provider', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]); }); - it('returns label suggestions on aggregation context and metric w/ selector', async () => { + it('returns label suggestions on aggregation context and metric w/ selector', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",foo="xx"}': ['bar'] } }); const value = Plain.deserialize('sum(metric{foo="xx"}) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(26).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 26, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-aggregation'], @@ -290,12 +281,14 @@ describe('Language completion provider', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); - it('returns label suggestions on aggregation context and metric w/o selector', async () => { + it('returns label suggestions on aggregation context and metric w/o selector', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['bar'] } }); const value = Plain.deserialize('sum(metric) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(16).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 16, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-aggregation'], @@ -305,16 +298,15 @@ describe('Language completion provider', () => { expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]); }); - it('returns label suggestions inside a multi-line aggregation context', async () => { + it('returns label suggestions inside a multi-line aggregation context', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, }); const value = Plain.deserialize('sum(\nmetric\n)\nby ()'); - const aggregationTextBlock = value.document.getBlocks().get(3); - const ed = new SlateEditor({ value }); - ed.moveToStartOfNode(aggregationTextBlock); - const valueWithSelection = ed.moveForward(4).value; - const result = await instance.provideCompletionItems({ + const aggregationTextBlock = value.document.getBlocksAsArray()[3]; + const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-aggregation'], @@ -329,14 +321,16 @@ describe('Language completion provider', () => { ]); }); - it('returns label suggestions inside an aggregation context with a range vector', async () => { + it('returns label suggestions inside an aggregation context with a range vector', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, }); const value = Plain.deserialize('sum(rate(metric[1h])) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(26).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 26, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-aggregation'], @@ -351,14 +345,16 @@ describe('Language completion provider', () => { ]); }); - it('returns label suggestions inside an aggregation context with a range vector and label', async () => { + it('returns label suggestions inside an aggregation context with a range vector and label', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] }, }); const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(42).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 42, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-aggregation'], @@ -373,14 +369,16 @@ describe('Language completion provider', () => { ]); }); - it('returns no suggestions inside an unclear aggregation context using alternate syntax', async () => { + it('returns no suggestions inside an unclear aggregation context using alternate syntax', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, }); const value = Plain.deserialize('sum by ()'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(8).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 8, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-aggregation'], @@ -390,14 +388,16 @@ describe('Language completion provider', () => { expect(result.suggestions).toEqual([]); }); - it('returns label suggestions inside an aggregation context using alternate syntax', async () => { + it('returns label suggestions inside an aggregation context using alternate syntax', () => { const instance = new LanguageProvider(datasource, { labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] }, }); const value = Plain.deserialize('sum by () (metric)'); - const ed = new SlateEditor({ value }); - const valueWithSelection = ed.moveForward(8).value; - const result = await instance.provideCompletionItems({ + const range = value.selection.merge({ + anchorOffset: 8, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: ['context-aggregation'], diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 3b06385efe8..09114c24738 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -23,18 +23,11 @@ import { import { Emitter } from 'app/core/core'; import TableModel from 'app/core/table_model'; -import { Value } from 'slate'; - -import { Editor } from '@grafana/slate-react'; export enum ExploreMode { Metrics = 'Metrics', Logs = 'Logs', } -export enum CompletionItemKind { - GroupTitle = 'GroupTitle', -} - export interface CompletionItem { /** * The label of this completion item. By default @@ -42,48 +35,40 @@ export interface CompletionItem { * this completion. */ label: string; - /** - * The kind of this completion item. An icon is chosen - * by the editor based on the kind. + * The kind of this completion item. Based on the kind + * an icon is chosen by the editor. */ - kind?: CompletionItemKind | string; - + kind?: string; /** * A human-readable string with additional information * about this item, like type or symbol information. */ detail?: string; - /** * A human-readable string, can be Markdown, that represents a doc-comment. */ documentation?: string; - /** * A string that should be used when comparing this item * with other items. When `falsy` the `label` is used. */ sortText?: string; - /** * A string that should be used when filtering a set of * completion items. When `falsy` the `label` is used. */ filterText?: string; - /** * A string or snippet that should be inserted in a document when selecting * this completion. When `falsy` the `label` is used. */ insertText?: string; - /** * Delete number of characters before the caret position, * by default the letters from the beginning of the word. */ deleteBackwards?: number; - /** * Number of steps to move after the insertion, can be negative. */ @@ -95,22 +80,18 @@ export interface CompletionItemGroup { * Label that will be displayed for all entries of this group. */ label: string; - /** * List of suggestions of this group. */ items: CompletionItem[]; - /** * If true, match only by prefix (and not mid-word). */ prefixMatch?: boolean; - /** * If true, do not filter items in this group based on the search. */ skipFilter?: boolean; - /** * If true, do not sort items. */ @@ -313,7 +294,7 @@ export interface HistoryItem { } export abstract class LanguageProvider { - datasource: DataSourceApi; + datasource: any; request: (url: string, params?: any) => Promise; /** * Returns startTask that resolves with a task list when main syntax is loaded. @@ -328,12 +309,13 @@ export interface TypeaheadInput { prefix: string; wrapperClasses: string[]; labelKey?: string; - value?: Value; - editor?: Editor; + //Should be Value from slate + value?: any; } export interface TypeaheadOutput { context?: string; + refresher?: Promise<{}>; suggestions: CompletionItemGroup[]; } diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss index fe0d9c10f05..50a58f8ff0c 100644 --- a/public/sass/components/_slate_editor.scss +++ b/public/sass/components/_slate_editor.scss @@ -30,9 +30,9 @@ .typeahead { position: absolute; z-index: auto; - top: 100px; - left: 160px; - //opacity: 0; + top: -10000px; + left: -10000px; + opacity: 0; border-radius: $border-radius; border: $panel-border; max-height: calc(66vh); @@ -43,7 +43,7 @@ list-style: none; background: $panel-bg; color: $text-color; - //transition: opacity 0.4s ease-out; + transition: opacity 0.4s ease-out; box-shadow: $typeahead-shadow; } diff --git a/tsconfig.json b/tsconfig.json index da5818b01de..cffbb331969 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,8 +31,7 @@ "typeRoots": ["node_modules/@types", "public/app/types"], "paths": { "app": ["app"], - "sass": ["sass"], - "@grafana/slate-react": ["../node_modules/@types/slate-react"] + "sass": ["sass"] }, "skipLibCheck": true, "preserveSymlinks": true diff --git a/yarn.lock b/yarn.lock index 1f6b4fe0335..3493e5c28db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1227,28 +1227,6 @@ unique-filename "^1.1.1" which "^1.3.1" -"@grafana/slate-react@0.22.9-grafana": - version "0.22.9-grafana" - resolved "https://registry.yarnpkg.com/@grafana/slate-react/-/slate-react-0.22.9-grafana.tgz#07f35f0ffc018f616b9f82fa6e5ba65fae75c6a0" - integrity sha512-9NYjwabVOUQ/e4Y/Wm+sgePM65rb/gju59D52t4O42HsIm9exXv+SLajEBF/HiLHzuH5V+5uuHajbzv0vuE2VA== - dependencies: - debug "^3.1.0" - get-window "^1.1.1" - is-window "^1.0.2" - lodash "^4.1.1" - memoize-one "^4.0.0" - prop-types "^15.5.8" - react-immutable-proptypes "^2.1.0" - selection-is-backward "^1.0.0" - slate-base64-serializer "^0.2.111" - slate-dev-environment "^0.2.2" - slate-hotkeys "^0.2.9" - slate-plain-serializer "^0.7.10" - slate-prop-types "^0.5.41" - slate-react-placeholder "^0.2.8" - tiny-invariant "^1.0.1" - tiny-warning "^0.0.3" - "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -3430,26 +3408,10 @@ version "7.0.11" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.11.tgz#6f28f005a36e779b7db0f1359b9fb9eef72aae88" -"@types/slate-plain-serializer@0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@types/slate-plain-serializer/-/slate-plain-serializer-0.6.1.tgz#c392ce51621f7c55df0976f161dcfca18bd559ee" - integrity sha512-5meyKFvmWH1T02j2dbAaY8kn/FNofxP79jV3TsfuLsUIeHkON5CroBxAyrgkYF4vHp+MVWZddI36Yvwl7Y0Feg== - dependencies: - "@types/slate" "*" - -"@types/slate-react@0.22.5": - version "0.22.5" - resolved "https://registry.yarnpkg.com/@types/slate-react/-/slate-react-0.22.5.tgz#a10796758aa6b3133e1c777959facbf8806959f7" - integrity sha512-WKJic5LlNRNUCnD6lEdlOZCcXWoDN8Ais2CmwVMn8pdt5Kh8hJsTYhXawNxOShPIOLVB+G+aVZNAXAAubEOpaw== - dependencies: - "@types/react" "*" - "@types/slate" "*" - immutable "^3.8.2" - -"@types/slate@*", "@types/slate@0.47.1": - version "0.47.1" - resolved "https://registry.yarnpkg.com/@types/slate/-/slate-0.47.1.tgz#6c66f82df085c764039eea2229be763f7e1906fd" - integrity sha512-2ZlnWI6/RYMXxeGFIeZtvmaXAeYAJh4ZVumziqVl77/liNEi9hOwkUTU2zFu+j/z21v385I2WVPl8sgadxfzXg== +"@types/slate@0.44.11": + version "0.44.11" + resolved "https://registry.yarnpkg.com/@types/slate/-/slate-0.44.11.tgz#152568096d1a089fa4c5bb03de1cf044a377206c" + integrity sha512-UnOGipgkE1+rq3L4JjsTO0b02FbT6b59+0/hkW/QFBDvCcxCSAdwdr9HYjXkMSCSVlcsEfdC/cz+XOaB+tGvlg== dependencies: "@types/react" "*" immutable "^3.8.2" @@ -4713,6 +4675,7 @@ bail@^1.0.0: balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= baron@3.0.3: version "3.0.3" @@ -4871,6 +4834,7 @@ boxen@^2.1.0: brace-expansion@^1.0.0, brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -5759,6 +5723,7 @@ compression@^1.5.2: concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= concat-stream@1.6.2, concat-stream@^1.4.6, concat-stream@^1.5.0: version "1.6.2" @@ -7189,7 +7154,6 @@ dir-glob@^2.0.0: direction@^0.1.5: version "0.1.5" resolved "https://registry.yarnpkg.com/direction/-/direction-0.1.5.tgz#ce5d797f97e26f8be7beff53f7dc40e1c1a9ec4c" - integrity sha1-zl15f5fib4vnvv9T99xA4cGp7Ew= discontinuous-range@1.0.0: version "1.0.0" @@ -7781,7 +7745,6 @@ esrecurse@^4.1.0: esrever@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/esrever/-/esrever-0.2.0.tgz#96e9d28f4f1b1a76784cd5d490eaae010e7407b8" - integrity sha1-lunSj08bGnZ4TNXUkOquAQ50B7g= estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0: version "4.2.0" @@ -8325,6 +8288,7 @@ for-in@^0.1.3: for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + integrity sha1-gQaNKVqBQuwKxybG4iAMMPttXoA= for-own@^0.1.3, for-own@^0.1.4: version "0.1.5" @@ -8485,6 +8449,7 @@ fs-write-stream-atomic@^1.0.8, fs-write-stream-atomic@~1.0.10: fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: version "1.2.9" @@ -8937,8 +8902,9 @@ got@^6.7.1: url-parse-lax "^1.0.0" graceful-fs@^4.1.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9: - version "4.1.15" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00" + version "4.2.2" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" + integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== "graceful-readlink@>= 1.0.0": version "1.0.1" @@ -9668,6 +9634,7 @@ infer-owner@^1.0.4: inflight@^1.0.4, inflight@~1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= dependencies: once "^1.3.0" wrappy "1" @@ -9976,6 +9943,10 @@ is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" +is-empty@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/is-empty/-/is-empty-1.2.0.tgz#de9bb5b278738a05a0b09a57e1fb4d4a341a9f6b" + is-equal-shallow@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" @@ -9989,6 +9960,7 @@ is-extendable@^0.1.0, is-extendable@^0.1.1: is-extendable@^1.0.0, is-extendable@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-1.0.1.tgz#a7470f9e426733d81bd81e1155264e3a3507cab4" + integrity sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA== dependencies: is-plain-object "^2.0.4" @@ -10050,7 +10022,7 @@ is-hexadecimal@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz#b6e710d7d07bb66b98cb8cece5c9b4921deeb835" -is-hotkey@0.1.4: +is-hotkey@0.1.4, is-hotkey@^0.1.1: version "0.1.4" resolved "https://registry.yarnpkg.com/is-hotkey/-/is-hotkey-0.1.4.tgz#c34d2c85d6ec8d09a871dcf71931c8067a824c7d" @@ -10305,6 +10277,7 @@ isobject@^2.0.0: isobject@^3.0.0, isobject@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8= isobject@^4.0.0: version "4.0.0" @@ -10941,7 +10914,7 @@ kew@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/kew/-/kew-0.7.0.tgz#79d93d2d33363d6fdd2970b335d9141ad591d79b" -keycode@^2.2.0: +keycode@^2.1.2, keycode@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" @@ -11364,8 +11337,9 @@ lockfile@^1.0.4: signal-exit "^3.0.2" lodash-es@^4.17.11, lodash-es@^4.2.1: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0" + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78" + integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ== lodash._baseuniq@~4.6.0: version "4.6.0" @@ -11382,7 +11356,7 @@ lodash._getnative@^3.0.0: version "3.9.1" resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5" -lodash._reinterpolate@^3.0.0, lodash._reinterpolate@~3.0.0: +lodash._reinterpolate@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= @@ -11453,8 +11427,9 @@ lodash.memoize@^4.1.2: resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" lodash.mergewith@^4.6.1: - version "4.6.1" - resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927" + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz#617121f89ac55f59047c7aec1ccd6654c6590f55" + integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ== lodash.once@^4.1.1: version "4.1.1" @@ -11477,7 +11452,7 @@ lodash.tail@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" -lodash.template@^4.0.2: +lodash.template@^4.0.2, lodash.template@^4.2.4: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== @@ -11485,20 +11460,12 @@ lodash.template@^4.0.2: lodash._reinterpolate "^3.0.0" lodash.templatesettings "^4.0.0" -lodash.template@^4.2.4: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.4.0.tgz#e73a0385c8355591746e020b99679c690e68fba0" - integrity sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A= - dependencies: - lodash._reinterpolate "~3.0.0" - lodash.templatesettings "^4.0.0" - lodash.templatesettings@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz#2b4d4e95ba440d915ff08bc899e4553666713316" - integrity sha1-K01OlbpEDZFf8IvImeRVNmZxMxY= + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" + integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== dependencies: - lodash._reinterpolate "~3.0.0" + lodash._reinterpolate "^3.0.0" lodash.throttle@^4.1.1: version "4.1.1" @@ -12001,6 +11968,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: "minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.4, minimatch@~3.0.0, minimatch@~3.0.2: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== dependencies: brace-expansion "^1.1.7" @@ -12021,6 +11989,7 @@ minimist-options@^3.0.1: minimist@0.0.8: version "0.0.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= minimist@1.1.x: version "1.1.3" @@ -12063,8 +12032,9 @@ mississippi@^3.0.0: through2 "^2.0.0" mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: for-in "^1.0.2" is-extendable "^1.0.1" @@ -12965,6 +12935,7 @@ on-headers@~1.0.2: once@^1.3.0, once@^1.3.1, once@^1.4.0, once@~1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= dependencies: wrappy "1" @@ -13387,6 +13358,7 @@ path-exists@^3.0.0: path-is-absolute@^1.0.0, path-is-absolute@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= path-is-inside@^1.0.1, path-is-inside@^1.0.2, path-is-inside@~1.0.2: version "1.0.2" @@ -14375,7 +14347,7 @@ pretty-hrtime@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" -prismjs@1.16.0, prismjs@^1.8.4, prismjs@~1.16.0: +prismjs@1.16.0, prismjs@^1.13.0, prismjs@^1.8.4, prismjs@~1.16.0: version "1.16.0" resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.16.0.tgz#406eb2c8aacb0f5f0f1167930cb83835d10a4308" optionalDependencies: @@ -15100,6 +15072,12 @@ react-popper@^1.3.3: typed-styles "^0.0.7" warning "^4.0.2" +react-portal@^3.1.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/react-portal/-/react-portal-3.2.0.tgz#4224e19b2b05d5cbe730a7ba0e34ec7585de0043" + dependencies: + prop-types "^15.5.8" + react-redux@5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.1.1.tgz#88e368682c7fa80e34e055cd7ac56f5936b0f52f" @@ -16507,55 +16485,81 @@ slash@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-2.0.0.tgz#de552851a1759df3a8f206535442f5ec4ddeab44" -slate-base64-serializer@^0.2.111: - version "0.2.111" - resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.111.tgz#22ba7d32aa4650f6bbd25c26ffe11f5d021959d6" - integrity sha512-pEsbxz4msVSCCCkn7rX+lHXxUj/oddcR4VsIYwWeQQLm9Uw7Ovxja4rQ/hVFcQqoU2DIjITRwBR9pv3RyS+PZQ== +slate-base64-serializer@^0.2.36: + version "0.2.102" + resolved "https://registry.yarnpkg.com/slate-base64-serializer/-/slate-base64-serializer-0.2.102.tgz#05cdb9149172944b55c8d0a0d14b4499a1c3b5a2" dependencies: isomorphic-base64 "^1.0.2" -slate-dev-environment@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.2.2.tgz#bd8946e1fe4cf5447060c84a362a1d026ed8b77f" - integrity sha512-JZ09llrRQu6JUsLJCUlGC0lB1r1qIAabAkSd454iyYBq6lDuY//Bypi3Jo8yzIfzZ4+mRLdQvl9e8MbeM9l48Q== +slate-dev-environment@^0.1.2, slate-dev-environment@^0.1.4: + version "0.1.6" + resolved "https://registry.yarnpkg.com/slate-dev-environment/-/slate-dev-environment-0.1.6.tgz#ff22b40ef4cc890ff7706b6b657abc276782424f" dependencies: is-in-browser "^1.1.3" -slate-hotkeys@^0.2.9: - version "0.2.9" - resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.2.9.tgz#0cc9eb750a49ab9ef11601305b7c82b5402348e3" - integrity sha512-y+C/s5vJEmBxo8fIqHmUcdViGwALL/A6Qow3sNG1OHYD5SI11tC2gfYtGbPh+2q0H7O4lufffCmFsP5bMaDHqA== - dependencies: - is-hotkey "0.1.4" - slate-dev-environment "^0.2.2" +slate-dev-logger@^0.1.39, slate-dev-logger@^0.1.43: + version "0.1.43" + resolved "https://registry.yarnpkg.com/slate-dev-logger/-/slate-dev-logger-0.1.43.tgz#77f6ca7207fcbf453a5516f3aa8b19794d1d26dc" -slate-plain-serializer@0.7.10, slate-plain-serializer@^0.7.10: - version "0.7.10" - resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.7.10.tgz#bc4a6942cf52fde826019bb1095dffd0dac8cc08" - integrity sha512-/QvMCQ0F3NzbnuoW+bxsLIChPdRgxBjQeGhYhpRGTVvlZCLOmfDvavhN6fHsuEwkvdwOmocNF30xT1WVlmibYg== +slate-hotkeys@^0.1.2: + version "0.1.4" + resolved "https://registry.yarnpkg.com/slate-hotkeys/-/slate-hotkeys-0.1.4.tgz#5b10b2a178affc60827f9284d4c0a5d7e5041ffe" + dependencies: + is-hotkey "^0.1.1" + slate-dev-environment "^0.1.4" -slate-prop-types@^0.5.41: +slate-plain-serializer@0.5.41, slate-plain-serializer@^0.5.17: version "0.5.41" - resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.5.41.tgz#42031881e2fef4fa978a96b9aad84b093b4a5219" - integrity sha512-fLcXlugO9btF5b/by+dA+n8fn2mET75VGWltqFNxGdl6ncyBtrGspWA7mLVRFSqQWOS/Ig4A3URCRumOBBCUfQ== + resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.5.41.tgz#dc2d219602c2cb8dc710ac660e108f3b3cc4dc80" + dependencies: + slate-dev-logger "^0.1.43" + +slate-prism@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/slate-prism/-/slate-prism-0.5.0.tgz#009eb74fea38ad76c64db67def7ea0884917adec" + dependencies: + prismjs "^1.13.0" -slate-react-placeholder@^0.2.8: - version "0.2.8" - resolved "https://registry.yarnpkg.com/slate-react-placeholder/-/slate-react-placeholder-0.2.8.tgz#973ac47c9a518a1418e89b6021b0f6120c07ce6f" - integrity sha512-CZZSg5usE2ZY/AYg06NVcL9Wia6hD/Mg0w4D4e9rPh6hkkFJg8LZXYMRz+6Q4v1dqHmzRsZ2Ixa0jRuiKXsMaQ== +slate-prop-types@^0.4.34: + version "0.4.67" + resolved "https://registry.yarnpkg.com/slate-prop-types/-/slate-prop-types-0.4.67.tgz#c6aa74195466546a44fcb85d1c7b15fefe36ce6b" -slate@0.47.8: - version "0.47.8" - resolved "https://registry.yarnpkg.com/slate/-/slate-0.47.8.tgz#1e987b74d8216d44ec56154f0e6d3c722ce21e6e" - integrity sha512-/Jt0eq4P40qZvtzeKIvNb+1N97zVICulGQgQoMDH0TI8h8B+5kqa1YeckRdRnuvfYJm3J/9lWn2V3J1PrF+hag== +slate-react@0.12.11: + version "0.12.11" + resolved "https://registry.yarnpkg.com/slate-react/-/slate-react-0.12.11.tgz#6d83e604634704757690a57dbd6aab282a964ad3" + dependencies: + debug "^3.1.0" + get-window "^1.1.1" + is-window "^1.0.2" + keycode "^2.1.2" + lodash "^4.1.1" + prop-types "^15.5.8" + react-immutable-proptypes "^2.1.0" + react-portal "^3.1.0" + selection-is-backward "^1.0.0" + slate-base64-serializer "^0.2.36" + slate-dev-environment "^0.1.2" + slate-dev-logger "^0.1.39" + slate-hotkeys "^0.1.2" + slate-plain-serializer "^0.5.17" + slate-prop-types "^0.4.34" + +slate-schema-violations@^0.1.12: + version "0.1.39" + resolved "https://registry.yarnpkg.com/slate-schema-violations/-/slate-schema-violations-0.1.39.tgz#854ab5624136419cef4c803b1823acabe11f1c15" + +slate@0.33.8: + version "0.33.8" + resolved "https://registry.yarnpkg.com/slate/-/slate-0.33.8.tgz#c2cd9906c446d010b15e9e28f6d1a01792c7a113" dependencies: debug "^3.1.0" direction "^0.1.5" esrever "^0.2.0" + is-empty "^1.0.0" is-plain-object "^2.0.4" lodash "^4.17.4" - tiny-invariant "^1.0.1" - tiny-warning "^0.0.3" + slate-dev-logger "^0.1.39" + slate-schema-violations "^0.1.12" type-of "^2.0.1" slice-ansi@0.0.4: @@ -17462,20 +17466,14 @@ tiny-emitter@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" -tiny-invariant@^1.0.1, tiny-invariant@^1.0.2: - version "1.0.6" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.6.tgz#b3f9b38835e36a41c843a3b0907a5a7b3755de73" - integrity sha512-FOyLWWVjG+aC0UqG76V53yAWdXfH8bO6FNmyZOuUrzDzK8DI3/JRY25UD7+g49JWM1LXwymsKERB+DzI0dTEQA== +tiny-invariant@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.0.4.tgz#346b5415fd93cb696b0c4e8a96697ff590f92463" tiny-relative-date@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07" -tiny-warning@^0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-0.0.3.tgz#1807eb4c5f81784a6354d58ea1d5024f18c6c81f" - integrity sha512-r0SSA5Y5IWERF9Xh++tFPx0jITBgGggOsRLDWWew6YRw/C2dr4uNO1fw1vanrBmHsICmPyMLNBZboTlxUmUuaA== - tiny-warning@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.2.tgz#1dfae771ee1a04396bdfde27a3adcebc6b648b28" @@ -17784,7 +17782,6 @@ type-name@^2.0.1: type-of@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972" - integrity sha1-5yoXQYllaOn2KDeNgW1pEvfyOXI= typed-styles@^0.0.7: version "0.0.7" @@ -18631,6 +18628,7 @@ wrap-ansi@^5.1.0: wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= write-file-atomic@2.4.1: version "2.4.1"