mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 00:47:38 -06:00
This reverts commit 601853fc84
.
This commit is contained in:
parent
601853fc84
commit
503dccb771
11
package.json
11
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",
|
||||
|
@ -149,7 +149,7 @@ export const getWebpackConfig: WebpackConfigurationGetter = options => {
|
||||
'emotion',
|
||||
'prismjs',
|
||||
'slate-plain-serializer',
|
||||
'@grafana/slate-react',
|
||||
'slate-react',
|
||||
'react',
|
||||
'react-dom',
|
||||
'react-redux',
|
||||
|
@ -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",
|
||||
|
@ -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(),
|
||||
],
|
||||
};
|
||||
|
@ -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<DataLinkInputProps> = ({ 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<DataLinkInputProps> = ({ 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<DataLinkInputProps> = ({ value, onChange, s
|
||||
</Portal>
|
||||
)}
|
||||
<Editor
|
||||
schema={SCHEMA}
|
||||
placeholder="http://your-grafana.com/d/000000010/annotations"
|
||||
value={linkUrl}
|
||||
onChange={onUrlChange}
|
||||
|
@ -2,4 +2,3 @@ export * from './components';
|
||||
export * from './types';
|
||||
export * from './utils';
|
||||
export * from './themes';
|
||||
export * from './slate-plugins';
|
||||
|
@ -1 +0,0 @@
|
||||
export { SlatePrism } from './slate-prism';
|
@ -1,3 +0,0 @@
|
||||
const TOKEN_MARK = 'prism-token';
|
||||
|
||||
export default TOKEN_MARK;
|
@ -1,160 +0,0 @@
|
||||
import Prism from 'prismjs';
|
||||
import { Block, Text, Decoration } from 'slate';
|
||||
import { Plugin } from '@grafana/slate-react';
|
||||
import Options, { OptionsFormat } from './options';
|
||||
import TOKEN_MARK from './TOKEN_MARK';
|
||||
|
||||
/**
|
||||
* A Slate plugin to highlight code syntax.
|
||||
*/
|
||||
export function SlatePrism(optsParam: OptionsFormat = {}): Plugin {
|
||||
const opts: Options = new Options(optsParam);
|
||||
|
||||
return {
|
||||
decorateNode: (node, editor, next) => {
|
||||
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;
|
||||
}
|
@ -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 <span className={className}>{props.children}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
@ -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);
|
||||
};
|
||||
|
@ -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"
|
||||
}
|
||||
|
@ -17,4 +17,45 @@ describe('<QueryField />', () => {
|
||||
const wrapper = shallow(<QueryField initialQuery="my query" />);
|
||||
expect(wrapper.find('div').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should execute query when enter is pressed and there are no suggestions visible', () => {
|
||||
const wrapper = shallow(<QueryField initialQuery="my query" />);
|
||||
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(<QueryField initialQuery="" />);
|
||||
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(<QueryField initialQuery="" />);
|
||||
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(<QueryField initialQuery="" />);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -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<TypeaheadOutput>;
|
||||
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<QueryFieldProps, QueryFieldState> {
|
||||
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<any>) {
|
||||
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<QueryFieldProps, QueryFieldS
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
this.updateMenu();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -110,7 +119,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
|
||||
componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
|
||||
const { initialQuery, syntax } = this.props;
|
||||
const { value } = this.state;
|
||||
const { value, suggestions } = this.state;
|
||||
|
||||
// if query changed from the outside
|
||||
if (initialQuery !== prevProps.initialQuery) {
|
||||
@ -119,17 +128,26 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
this.setState({ value: makeValue(initialQuery || '', syntax) });
|
||||
}
|
||||
}
|
||||
|
||||
// Only update menu location when suggestion existence or text/selection changed
|
||||
if (value !== prevState.value || hasSuggestions(suggestions) !== hasSuggestions(prevState.suggestions)) {
|
||||
this.updateMenu();
|
||||
}
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: QueryFieldProps) {
|
||||
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
|
||||
// Need a bogus edit to re-render the editor after syntax has fully loaded
|
||||
const editor = this.editor.insertText(' ').deleteBackward(1);
|
||||
this.onChange(editor.value, true);
|
||||
const change = this.state.value
|
||||
.change()
|
||||
.insertText(' ')
|
||||
.deleteBackward();
|
||||
|
||||
this.onChange(change, true);
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (value: Value, invokeParentOnValueChanged?: boolean) => {
|
||||
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<QueryFieldProps, QueryFieldS
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show suggest menu on text input
|
||||
if (documentChanged && value.selection.isCollapsed) {
|
||||
// Need one paint to allow DOM-based typeahead rules to work
|
||||
window.requestAnimationFrame(this.handleTypeahead);
|
||||
} else if (!this.resetTimer) {
|
||||
this.resetTypeahead();
|
||||
}
|
||||
};
|
||||
|
||||
updateLogsHighlights = () => {
|
||||
@ -168,18 +194,475 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
}
|
||||
};
|
||||
|
||||
handleBlur = (event: Event, editor: CoreEditor, next: Function) => {
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
editor.blur();
|
||||
onClickMenu = (item: CompletionItem) => {
|
||||
// Manually triggering change
|
||||
const change = this.applyTypeahead(this.state.value.change(), item);
|
||||
this.onChange(change, true);
|
||||
};
|
||||
|
||||
return next();
|
||||
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 (
|
||||
<Portal origin={portalOrigin}>
|
||||
<TypeaheadWithTheme
|
||||
menuRef={this.menuRef}
|
||||
selectedItem={selectedItem}
|
||||
onClickItem={this.onClickMenu}
|
||||
prefix={typeaheadPrefix}
|
||||
groupedItems={suggestions}
|
||||
typeaheadIndex={typeaheadIndex}
|
||||
/>
|
||||
</Portal>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
handleCut = (event: ClipboardEvent, change: Change) => {
|
||||
this.handleCopy(event, change);
|
||||
change.deleteAtRange(change.value.selection);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -187,20 +670,19 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
const wrapperClassName = classnames('slate-query-field__wrapper', {
|
||||
'slate-query-field__wrapper--disabled': disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={wrapperClassName}>
|
||||
<div className="slate-query-field">
|
||||
{this.renderMenu()}
|
||||
<Editor
|
||||
ref={editor => (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<QueryFieldProps, QueryFieldS
|
||||
}
|
||||
}
|
||||
|
||||
interface PortalProps {
|
||||
index?: number;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
class Portal extends React.PureComponent<PortalProps, {}> {
|
||||
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;
|
||||
|
@ -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<Props, State> {
|
||||
listRef = createRef<FixedSizeList>();
|
||||
listRef: any = createRef();
|
||||
documentationRef: any = createRef();
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
@ -39,173 +35,97 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
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<Props>, prevState: Readonly<State>) => {
|
||||
if (prevState.typeaheadIndex !== this.state.typeaheadIndex && this.listRef && this.listRef.current) {
|
||||
if (this.state.typeaheadIndex === 1) {
|
||||
componentDidUpdate = (prevProps: Readonly<Props>) => {
|
||||
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,
|
||||
});
|
||||
refreshDocumentation = () => {
|
||||
if (!this.documentationRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this.state.allItems.findIndex(item => item === this.props.selectedItem);
|
||||
const item = this.state.allItems[index];
|
||||
|
||||
if (item) {
|
||||
this.documentationRef.current.refresh(item);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseEnter = (item: CompletionItem) => {
|
||||
this.documentationRef.current.refresh(item);
|
||||
};
|
||||
|
||||
onMouseLeave = () => {
|
||||
this.setState({
|
||||
hoveredItem: null,
|
||||
});
|
||||
this.documentationRef.current.hide();
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
insertSuggestion = () => {
|
||||
this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
|
||||
};
|
||||
|
||||
get menuPosition(): CSSProperties {
|
||||
// Exit for unit tests
|
||||
if (!window.getSelection) {
|
||||
return {};
|
||||
}
|
||||
|
||||
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`,
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
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 (
|
||||
<Portal origin={origin} isOpen={isOpen}>
|
||||
<ul className="typeahead" style={this.menuPosition}>
|
||||
<FixedSizeList
|
||||
ref={this.listRef}
|
||||
itemCount={allItems.length}
|
||||
itemSize={itemHeight}
|
||||
itemKey={index => {
|
||||
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;
|
||||
}
|
||||
<ul className="typeahead" ref={menuRef}>
|
||||
<TypeaheadInfo
|
||||
ref={this.documentationRef}
|
||||
width={listWidth}
|
||||
height={listHeight}
|
||||
theme={theme}
|
||||
initialItem={selectedItem}
|
||||
/>
|
||||
<FixedSizeList
|
||||
ref={this.listRef}
|
||||
itemCount={allItems.length}
|
||||
itemSize={itemHeight}
|
||||
itemKey={index => {
|
||||
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 (
|
||||
<TypeaheadItem
|
||||
onClickItem={() => this.props.onSelectSuggestion(item)}
|
||||
isSelected={allItems[typeaheadIndex] === item}
|
||||
item={item}
|
||||
prefix={prefix}
|
||||
style={style}
|
||||
onMouseEnter={() => this.onMouseEnter(index)}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</FixedSizeList>
|
||||
</ul>
|
||||
|
||||
{showDocumentation && (
|
||||
<TypeaheadInfo
|
||||
width={listWidth}
|
||||
height={listHeight}
|
||||
theme={theme}
|
||||
item={allItems[hoveredItem ? hoveredItem : typeaheadIndex]}
|
||||
/>
|
||||
)}
|
||||
</Portal>
|
||||
return (
|
||||
<TypeaheadItem
|
||||
onClickItem={onClickItem}
|
||||
isSelected={selectedItem === item}
|
||||
item={item}
|
||||
prefix={prefix}
|
||||
style={style}
|
||||
onMouseEnter={this.onMouseEnter}
|
||||
onMouseLeave={this.onMouseLeave}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</FixedSizeList>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const TypeaheadWithTheme = withTheme(Typeahead);
|
||||
|
||||
interface PortalProps {
|
||||
index?: number;
|
||||
isOpen: boolean;
|
||||
origin: string;
|
||||
}
|
||||
|
||||
class Portal extends React.PureComponent<PortalProps, {}> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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<Props> {
|
||||
interface State {
|
||||
item: CompletionItem;
|
||||
}
|
||||
|
||||
export class TypeaheadInfo extends PureComponent<Props, State> {
|
||||
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<Props> {
|
||||
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<Props> {
|
||||
};
|
||||
};
|
||||
|
||||
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 : '';
|
||||
|
@ -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: 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 (
|
||||
<li className={itemGroupTitleClassName} style={style}>
|
||||
<span>{label}</span>
|
||||
@ -76,13 +80,7 @@ export const TypeaheadItem: FunctionComponent<Props> = (props: Props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<li
|
||||
className={className}
|
||||
style={style}
|
||||
onMouseDown={onClickItem}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
<li className={className} onClick={onClick} style={style} onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
|
||||
<Highlighter textToHighlight={label} searchWords={[prefix]} highlightClassName={highlightClassName} />
|
||||
</li>
|
||||
);
|
||||
|
39
public/app/features/explore/slate-plugins/braces.test.ts
Normal file
39
public/app/features/explore/slate-plugins/braces.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
@ -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<Editor>(<Editor value={value} />);
|
||||
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<Editor>(<Editor value={value} />);
|
||||
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<Editor>(<Editor value={value} />);
|
||||
const event = new window.KeyboardEvent('keydown', { key: 'Backspace' });
|
||||
const handled = handler(event as Event, editor.instance().moveForward(5) as any, nextMock);
|
||||
expect(handled).toBeFalsy();
|
||||
});
|
||||
});
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
39
public/app/features/explore/slate-plugins/clear.test.ts
Normal file
39
public/app/features/explore/slate-plugins/clear.test.ts
Normal file
@ -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 ');
|
||||
});
|
||||
});
|
@ -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<Editor>(<Editor value={value} />);
|
||||
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<Editor>(<Editor value={value} />);
|
||||
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<Editor>(<Editor value={value} />);
|
||||
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 ');
|
||||
});
|
||||
});
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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<Editor>(<Editor value={value} />);
|
||||
handler({ key: 'Enter', preventDefault: () => {} } as KeyboardEvent, editor.instance() as any, () => {});
|
||||
expect(mockHandler).toBeCalled();
|
||||
});
|
||||
});
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
@ -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<TypeaheadOutput>;
|
||||
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}
|
||||
<TypeaheadWithTheme
|
||||
menuRef={(el: Typeahead) => (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<TypeaheadOutput>,
|
||||
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
|
||||
);
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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<Props, State> {
|
||||
super(props, context);
|
||||
|
||||
this.plugins = [
|
||||
SlatePrism({
|
||||
PluginPrism({
|
||||
onlyIn: (node: any) => node.type === 'code_block',
|
||||
getSyntax: (node: any) => 'lucene',
|
||||
}),
|
||||
|
@ -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 [
|
||||
|
@ -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<any, any> {
|
||||
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<any, any> {
|
||||
}
|
||||
};
|
||||
|
||||
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<any, any> {
|
||||
}
|
||||
|
||||
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<any, any> {
|
||||
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<any, any> {
|
||||
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<any, any> {
|
||||
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<any, any> {
|
||||
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<any, any> {
|
||||
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);
|
||||
};
|
||||
|
||||
|
@ -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<LokiQueryFieldFormProps> = ({
|
||||
datasource,
|
||||
@ -9,7 +8,7 @@ export const LokiQueryField: FunctionComponent<LokiQueryFieldFormProps> = ({
|
||||
...otherProps
|
||||
}) => {
|
||||
const { isSyntaxReady, setActiveOption, refreshLabels, ...syntaxProps } = useLokiSyntax(
|
||||
datasource.languageProvider as LokiLanguageProvider,
|
||||
datasource.languageProvider,
|
||||
datasourceStatus,
|
||||
otherProps.absoluteRange
|
||||
);
|
||||
|
@ -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<DataSourceApi<LokiQuery>, 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<LokiQueryFieldFormProps> {
|
||||
plugins: Plugin[];
|
||||
plugins: any[];
|
||||
modifiedSearch: string;
|
||||
modifiedQuery: string;
|
||||
|
||||
@ -88,9 +82,9 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
SlatePrism({
|
||||
onlyIn: (node: Node) => 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<LokiQueryFieldFormPr
|
||||
}
|
||||
};
|
||||
|
||||
onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
|
||||
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;
|
||||
|
||||
const result = await lokiLanguageProvider.provideCompletionItems(
|
||||
// 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 = 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<LokiQueryFieldFormPr
|
||||
datasource,
|
||||
datasourceStatus,
|
||||
} = this.props;
|
||||
const lokiLanguageProvider = datasource.languageProvider as LokiLanguageProvider;
|
||||
const cleanText = datasource.languageProvider ? lokiLanguageProvider.cleanText : undefined;
|
||||
const cleanText = datasource.languageProvider ? datasource.languageProvider.cleanText : undefined;
|
||||
const hasLogLabels = logLabelOptions && logLabelOptions.length > 0;
|
||||
const chooserText = getChooserText(syntaxLoaded, hasLogLabels, datasourceStatus);
|
||||
const buttonDisabled = !syntaxLoaded || datasourceStatus === DataSourceStatus.Disconnected;
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -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: '',
|
||||
|
@ -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<TypeaheadOutput> {
|
||||
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> {
|
||||
): TypeaheadOutput {
|
||||
let context: string;
|
||||
const suggestions = [];
|
||||
let refresher: Promise<any> = 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<LokiQuery[]> {
|
||||
|
@ -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) => {
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { Grammar } from 'prismjs';
|
||||
|
||||
/* tslint:disable max-line-length */
|
||||
|
||||
const tokenizer: Grammar = {
|
||||
const tokenizer = {
|
||||
comment: {
|
||||
pattern: /(^|[^\n])#.*/,
|
||||
lookbehind: true,
|
||||
|
@ -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<PrometheusDatasource, PromQuery, PromOptions> {
|
||||
history: Array<HistoryItem<PromQuery>>;
|
||||
history: HistoryItem[];
|
||||
}
|
||||
|
||||
interface PromQueryFieldState {
|
||||
@ -115,7 +113,7 @@ interface PromQueryFieldState {
|
||||
|
||||
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
|
||||
plugins: any[];
|
||||
languageProvider: PromQlLanguageProvider;
|
||||
languageProvider: any;
|
||||
languageProviderInitializationPromise: CancelablePromise<any>;
|
||||
|
||||
constructor(props: PromQueryFieldProps, context: React.Context<any>) {
|
||||
@ -127,7 +125,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
|
||||
this.plugins = [
|
||||
BracesPlugin(),
|
||||
SlatePrism({
|
||||
PluginPrism({
|
||||
onlyIn: (node: any) => node.type === 'code_block',
|
||||
getSyntax: (node: any) => 'promql',
|
||||
}),
|
||||
@ -254,7 +252,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
return;
|
||||
}
|
||||
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.syntax;
|
||||
Prism.languages[PRISM_SYNTAX] = this.languageProvider.getSyntax();
|
||||
Prism.languages[PRISM_SYNTAX][METRIC_MARK] = {
|
||||
alias: 'variable',
|
||||
pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
|
||||
@ -274,20 +272,26 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
||||
this.setState({ metricsOptions, syntaxLoaded: true });
|
||||
};
|
||||
|
||||
onTypeahead = async (typeahead: TypeaheadInput): Promise<TypeaheadOutput> => {
|
||||
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;
|
||||
|
||||
const result = await this.languageProvider.provideCompletionItems(
|
||||
// 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 = 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;
|
||||
};
|
||||
|
@ -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<any>;
|
||||
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<HistoryItem<PromQuery>> } = { history: [] }
|
||||
): Promise<TypeaheadOutput> => {
|
||||
// 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<HistoryItem<PromQuery>> }): 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<any> = 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<TypeaheadOutput> => {
|
||||
getLabelCompletionItems({ text, wrapperClasses, labelKey, value }: TypeaheadInput): TypeaheadOutput {
|
||||
let context: string;
|
||||
let refresher: Promise<any> = 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 {
|
||||
|
@ -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]);
|
||||
}
|
||||
});
|
||||
|
@ -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<HistoryItem<PromQuery>> = [
|
||||
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'],
|
||||
|
@ -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<TQuery extends DataQuery = DataQuery> {
|
||||
}
|
||||
|
||||
export abstract class LanguageProvider {
|
||||
datasource: DataSourceApi;
|
||||
datasource: any;
|
||||
request: (url: string, params?: any) => Promise<any>;
|
||||
/**
|
||||
* 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[];
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
220
yarn.lock
220
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==
|
||||
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-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.4"
|
||||
slate-dev-environment "^0.2.2"
|
||||
is-hotkey "^0.1.1"
|
||||
slate-dev-environment "^0.1.4"
|
||||
|
||||
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-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-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-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@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-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-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"
|
||||
|
Loading…
Reference in New Issue
Block a user