Chore: Moves QueryField to @grafana/ui (#19678)

Closes #19626
This commit is contained in:
kay delaney
2019-10-31 08:27:01 +00:00
committed by David
parent 69906f73a2
commit 3e8c00dad1
39 changed files with 417 additions and 392 deletions

View File

@@ -19,9 +19,18 @@ import { renderUrl } from 'app/core/utils/url';
import store from 'app/core/store';
import kbn from 'app/core/utils/kbn';
import { getNextRefIdChar } from './query';
// Types
import { DataQuery, DataSourceApi, DataQueryError, DataQueryRequest, PanelModel, RefreshPicker } from '@grafana/ui';
import { ExploreUrlState, HistoryItem, QueryTransaction, QueryOptions, ExploreMode } from 'app/types/explore';
import {
DataQuery,
DataSourceApi,
DataQueryError,
DataQueryRequest,
PanelModel,
RefreshPicker,
HistoryItem,
} from '@grafana/ui';
import { ExploreUrlState, QueryTransaction, QueryOptions, ExploreMode } from 'app/types/explore';
import { config } from '../config';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { QueryField } from './QueryField';
describe('<QueryField />', () => {
it('should render with null initial value', () => {
const wrapper = shallow(<QueryField query={null} />);
expect(wrapper.find('div').exists()).toBeTruthy();
});
it('should render with empty initial value', () => {
const wrapper = shallow(<QueryField query="" />);
expect(wrapper.find('div').exists()).toBeTruthy();
});
it('should render with initial value', () => {
const wrapper = shallow(<QueryField query="my query" />);
expect(wrapper.find('div').exists()).toBeTruthy();
});
});

View File

@@ -1,219 +0,0 @@
import _ from 'lodash';
import React, { Context } from 'react';
import { Value, Editor as CoreEditor } from 'slate';
import { Editor, Plugin } from '@grafana/slate-react';
import Plain from 'slate-plain-serializer';
import classnames from 'classnames';
import { 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 { makeValue, SCHEMA } from '@grafana/ui';
export interface QueryFieldProps {
additionalPlugins?: Plugin[];
cleanText?: (text: string) => string;
disabled?: boolean;
// We have both value and local state. This is usually an antipattern but we need to keep local state
// for perf reasons and also have outside value in for example in Explore redux that is mutable from logs
// creating a two way binding.
query: string | null;
onRunQuery?: () => void;
onChange?: (value: string) => void;
onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
placeholder?: string;
portalOrigin?: string;
syntax?: string;
syntaxLoaded?: boolean;
}
export interface QueryFieldState {
suggestions: CompletionItemGroup[];
typeaheadContext: string | null;
typeaheadPrefix: string;
typeaheadText: string;
value: Value;
}
export interface TypeaheadInput {
prefix: string;
selection?: Selection;
text: string;
value: Value;
wrapperClasses: string[];
labelKey?: string;
}
/**
* Renders an editor field.
* Pass initial value as initialQuery and listen to changes in props.onValueChanged.
* This component can only process strings. Internally it uses Slate Value.
* Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example.
*/
export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
plugins: Plugin[];
resetTimer: NodeJS.Timer;
mounted: boolean;
runOnChangeDebounced: Function;
editor: Editor;
lastExecutedValue: Value | null = null;
constructor(props: QueryFieldProps, context: Context<any>) {
super(props, context);
this.runOnChangeDebounced = _.debounce(this.runOnChange, 500);
const { onTypeahead, cleanText, portalOrigin, onWillApplySuggestion } = props;
// Base plugins
this.plugins = [
NewlinePlugin(),
SuggestionsPlugin({ onTypeahead, cleanText, portalOrigin, onWillApplySuggestion }),
ClearPlugin(),
RunnerPlugin({ handler: this.runOnChangeAndRunQuery }),
SelectionShortcutsPlugin(),
IndentationPlugin(),
ClipboardPlugin(),
...(props.additionalPlugins || []),
].filter(p => p);
this.state = {
suggestions: [],
typeaheadContext: null,
typeaheadPrefix: '',
typeaheadText: '',
value: makeValue(props.query || '', props.syntax),
};
}
componentDidMount() {
this.mounted = true;
}
componentWillUnmount() {
this.mounted = false;
clearTimeout(this.resetTimer);
}
componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
const { query, syntax } = this.props;
const { value } = this.state;
// Handle two way binging between local state and outside prop.
// if query changed from the outside
if (query !== prevProps.query) {
// and we have a version that differs
if (query !== Plain.serialize(value)) {
this.setState({ value: makeValue(query || '', syntax) });
}
}
}
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);
}
}
/**
* Update local state, propagate change upstream and optionally run the query afterwards.
*/
onChange = (value: Value, runQuery?: boolean) => {
const documentChanged = value.document !== this.state.value.document;
const prevValue = this.state.value;
// Update local state with new value and optionally change value upstream.
this.setState({ value }, () => {
// The diff is needed because the actual value of editor have much more metadata (for example text selection)
// that is not passed upstream so every change of editor value does not mean change of the query text.
if (documentChanged) {
const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value);
if (textChanged && runQuery) {
this.runOnChangeAndRunQuery();
}
if (textChanged && !runQuery) {
// Debounce change propagation by default for perf reasons.
this.runOnChangeDebounced();
}
}
});
};
runOnChange = () => {
const { onChange } = this.props;
if (onChange) {
onChange(Plain.serialize(this.state.value));
}
};
runOnRunQuery = () => {
const { onRunQuery } = this.props;
if (onRunQuery) {
onRunQuery();
this.lastExecutedValue = this.state.value;
}
};
runOnChangeAndRunQuery = () => {
// onRunQuery executes query from Redux in Explore so it needs to be updated sync in case we want to run
// the query.
this.runOnChange();
this.runOnRunQuery();
};
/**
* We need to handle blur events here mainly because of dashboard panels which expect to have query executed on blur.
*/
handleBlur = (event: Event, editor: CoreEditor, next: Function) => {
const previousValue = this.lastExecutedValue ? Plain.serialize(this.lastExecutedValue) : null;
const currentValue = Plain.serialize(editor.value);
if (previousValue !== currentValue) {
this.runOnChangeAndRunQuery();
}
return next();
};
render() {
const { disabled } = this.props;
const wrapperClassName = classnames('slate-query-field__wrapper', {
'slate-query-field__wrapper--disabled': disabled,
});
return (
<div className={wrapperClassName}>
<div className="slate-query-field">
<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);
}}
placeholder={this.props.placeholder}
plugins={this.plugins}
spellCheck={false}
value={this.state.value}
/>
</div>
</div>
);
}
}
export default QueryField;

View File

@@ -13,8 +13,8 @@ import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/act
// Types
import { StoreState } from 'app/types';
import { TimeRange, AbsoluteTimeRange, LoadingState } from '@grafana/data';
import { DataQuery, DataSourceApi, QueryFixAction, PanelData } from '@grafana/ui';
import { HistoryItem, ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
import { DataQuery, DataSourceApi, QueryFixAction, PanelData, HistoryItem } from '@grafana/ui';
import { ExploreItemState, ExploreId, ExploreMode } from 'app/types/explore';
import { Emitter } from 'app/core/utils/emitter';
import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes';
import QueryStatus from './QueryStatus';

View File

@@ -1,226 +0,0 @@
import React, { createRef } from 'react';
import ReactDOM from 'react-dom';
import _ from 'lodash';
import { FixedSizeList } from 'react-window';
import { Themeable, withTheme } from '@grafana/ui';
import { CompletionItem, CompletionItemKind, 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[];
prefix?: string;
menuRef?: (el: Typeahead) => void;
onSelectSuggestion?: (suggestion: CompletionItem) => void;
isOpen?: boolean;
}
interface State {
allItems: CompletionItem[];
listWidth: number;
listHeight: number;
itemHeight: number;
hoveredItem: number;
typeaheadIndex: number;
}
export class Typeahead extends React.PureComponent<Props, State> {
listRef = createRef<FixedSizeList>();
constructor(props: Props) {
super(props);
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 };
}
componentDidMount = () => {
this.props.menuRef(this);
document.addEventListener('selectionchange', this.handleSelectionChange);
};
componentWillUnmount = () => {
document.removeEventListener('selectionchange', this.handleSelectionChange);
};
handleSelectionChange = () => {
this.forceUpdate();
};
componentDidUpdate = (prevProps: Readonly<Props>, prevState: Readonly<State>) => {
if (prevState.typeaheadIndex !== this.state.typeaheadIndex && this.listRef && this.listRef.current) {
if (this.state.typeaheadIndex === 1) {
this.listRef.current.scrollToItem(0); // special case for handling the first group label
return;
}
this.listRef.current.scrollToItem(this.state.typeaheadIndex);
}
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 });
}
};
onMouseEnter = (index: number) => {
this.setState({
hoveredItem: index,
});
};
onMouseLeave = () => {
this.setState({
hoveredItem: null,
});
};
moveMenuIndex = (moveAmount: number) => {
const itemCount = this.state.allItems.length;
if (itemCount) {
// Select next suggestion
event.preventDefault();
let newTypeaheadIndex = modulo(this.state.typeaheadIndex + moveAmount, itemCount);
if (this.state.allItems[newTypeaheadIndex].kind === CompletionItemKind.GroupTitle) {
newTypeaheadIndex = modulo(newTypeaheadIndex + moveAmount, itemCount);
}
this.setState({
typeaheadIndex: newTypeaheadIndex,
});
return;
}
};
insertSuggestion = () => {
this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
};
get menuPosition(): string {
// 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 `position: absolute; display: flex; top: ${rect.top + scrollY + rect.height + 6}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;
return (
<Portal origin={origin} isOpen={isOpen} style={this.menuPosition}>
<ul className="typeahead">
<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>
);
}
}
export const TypeaheadWithTheme = withTheme(Typeahead);
interface PortalProps {
index?: number;
isOpen: boolean;
origin: string;
style: string;
}
class Portal extends React.PureComponent<PortalProps, {}> {
node: HTMLElement;
constructor(props: PortalProps) {
super(props);
const { index = 0, origin = 'query', style } = props;
this.node = document.createElement('div');
this.node.setAttribute('style', style);
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) {
this.node.setAttribute('style', this.props.style);
this.node.classList.add(`slate-typeahead--open`);
return ReactDOM.createPortal(this.props.children, this.node);
} else {
this.node.classList.remove(`slate-typeahead--open`);
}
return null;
}
}

View File

@@ -1,63 +0,0 @@
import React, { PureComponent } from 'react';
import { css, cx } from 'emotion';
import { Themeable, selectThemeVariant } from '@grafana/ui';
import { CompletionItem } from 'app/types/explore';
interface Props extends Themeable {
item: CompletionItem;
width: number;
height: number;
}
export class TypeaheadInfo extends PureComponent<Props> {
constructor(props: Props) {
super(props);
}
getStyles = (visible: boolean) => {
const { height, theme } = this.props;
return {
typeaheadItem: css`
label: type-ahead-item;
padding: ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.md};
border-radius: ${theme.border.radius.md};
border: ${selectThemeVariant(
{ light: `solid 1px ${theme.colors.gray5}`, dark: `solid 1px ${theme.colors.dark1}` },
theme.type
)};
overflow-y: scroll;
overflow-x: hidden;
outline: none;
background: ${selectThemeVariant({ light: theme.colors.white, dark: theme.colors.dark4 }, theme.type)};
color: ${theme.colors.text};
box-shadow: ${selectThemeVariant(
{ light: `0 5px 10px 0 ${theme.colors.gray5}`, dark: `0 5px 10px 0 ${theme.colors.black}` },
theme.type
)};
visibility: ${visible === true ? 'visible' : 'hidden'};
width: 250px;
height: ${height + parseInt(theme.spacing.xxs, 10)}px;
position: relative;
`,
};
};
render() {
const { item } = this.props;
const visible = item && !!item.documentation;
const label = item ? item.label : '';
const documentation = item && item.documentation ? item.documentation : '';
const styles = this.getStyles(visible);
return (
<div className={cx([styles.typeaheadItem])}>
<b>{label}</b>
<hr />
<span>{documentation}</span>
</div>
);
}
}

View File

@@ -1,89 +0,0 @@
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';
interface Props {
isSelected: boolean;
item: CompletionItem;
style: any;
prefix?: string;
onClickItem?: (event: React.MouseEvent) => void;
onMouseEnter?: () => void;
onMouseLeave?: () => void;
}
const getStyles = (theme: GrafanaTheme) => ({
typeaheadItem: css`
label: type-ahead-item;
height: auto;
font-family: ${theme.typography.fontFamily.monospace};
padding: ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.sm} ${theme.spacing.md};
font-size: ${theme.typography.size.sm};
text-overflow: ellipsis;
overflow: hidden;
z-index: 1;
display: block;
white-space: nowrap;
cursor: pointer;
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};
border-bottom: 1px solid ${theme.colors.yellow};
padding: inherit;
background: inherit;
`,
typeaheadItemGroupTitle: css`
label: type-ahead-item-group-title;
color: ${theme.colors.textWeak};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.lg};
padding: ${theme.spacing.sm};
`,
});
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 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) {
return (
<li className={itemGroupTitleClassName} style={style}>
<span>{label}</span>
</li>
);
}
return (
<li
className={className}
style={style}
onMouseDown={onClickItem}
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
>
<Highlighter textToHighlight={label} searchWords={[prefix]} highlightClassName={highlightClassName} />
</li>
);
};

View File

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

View File

@@ -1,70 +0,0 @@
import { Plugin } from '@grafana/slate-react';
import { Editor as CoreEditor } from 'slate';
const BRACES: any = {
'[': ']',
'{': '}',
'(': ')',
};
export default function BracesPlugin(): Plugin {
return {
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
const { value } = editor;
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;
// If text is selected, wrap selected text in parens
if (value.selection.isExpanded) {
editor
.insertTextByKey(startKey, startOffset, event.key)
.insertTextByKey(endKey, endOffset + 1, BRACES[event.key])
.moveEndBackward(1);
} else if (
focusOffset === text.length ||
text[focusOffset] === ' ' ||
Object.values(BRACES).includes(text[focusOffset])
) {
editor.insertText(`${event.key}${BRACES[event.key]}`).moveBackward(1);
} else {
editor.insertText(event.key);
}
return true;
}
case 'Backspace': {
const text = value.anchorText.text;
const offset = value.selection.anchor.offset;
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)
.focus();
return true;
}
}
default: {
break;
}
}
return next();
},
};
}

View File

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

View File

@@ -1,27 +0,0 @@
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 {
return {
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
const value = editor.value;
if (value.selection.isExpanded) {
return next();
}
if (event.key === 'k' && event.ctrlKey) {
event.preventDefault();
const text = value.anchorText.text;
const offset = value.selection.anchor.offset;
const length = text.length;
const forward = length - offset;
editor.deleteForward(forward);
return true;
}
return next();
},
};
}

View File

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

View File

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

View File

@@ -1,41 +0,0 @@
import { Plugin } from '@grafana/slate-react';
import { Editor as CoreEditor } from 'slate';
function getIndent(text: string) {
let offset = text.length - text.trimLeft().length;
if (offset) {
let indent = text[0];
while (--offset) {
indent += text[0];
}
return indent;
}
return '';
}
export default function NewlinePlugin(): Plugin {
return {
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
const value = editor.value;
if (value.selection.isExpanded) {
return next();
}
if (event.key === 'Enter' && event.shiftKey) {
event.preventDefault();
const { startBlock } = value;
const currentLineText = startBlock.text;
const indent = getIndent(currentLineText);
return editor
.splitBlock()
.insertText(indent)
.focus();
}
return next();
},
};
}

View File

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

View File

@@ -1,17 +0,0 @@
import { Editor as SlateEditor } from 'slate';
export default function RunnerPlugin({ handler }: any) {
return {
onKeyDown(event: KeyboardEvent, editor: SlateEditor, next: Function) {
// Handle enter
if (handler && event.key === 'Enter' && !event.shiftKey) {
// Submit on Enter
event.preventDefault();
handler(event);
return true;
}
return next();
},
};
}

View File

@@ -1,31 +0,0 @@
import { Plugin } from '@grafana/slate-react';
import { Editor as CoreEditor } from 'slate';
import { isKeyHotkey } from 'is-hotkey';
const isSelectLineHotkey = isKeyHotkey('mod+l');
// Clears the rest of the line after the caret
export default function SelectionShortcutsPlugin(): Plugin {
return {
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
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;
},
};
}

View File

@@ -1,320 +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 { 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;
// Commands added to the editor by this plugin.
interface SuggestionsPluginCommands {
selectSuggestion: (suggestion: CompletionItem) => CoreEditor;
applyTypeahead: (suggestion: CompletionItem) => CoreEditor;
}
export interface SuggestionsState {
groupedItems: CompletionItemGroup[];
typeaheadPrefix: string;
typeaheadContext: string;
typeaheadText: string;
}
export default function SuggestionsPlugin({
onTypeahead,
cleanText,
onWillApplySuggestion,
portalOrigin,
}: {
onTypeahead: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
cleanText?: (text: string) => string;
onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
portalOrigin: string;
}): SlatePlugin {
let typeaheadRef: Typeahead;
let state: SuggestionsState = {
groupedItems: [],
typeaheadPrefix: '',
typeaheadContext: '',
typeaheadText: '',
};
const handleTypeaheadDebounced = debounce(handleTypeahead, TYPEAHEAD_DEBOUNCE);
const setState = (update: Partial<SuggestionsState>) => {
state = {
...state,
...update,
};
};
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();
typeaheadRef.moveMenuIndex(event.key === 'ArrowDown' ? 1 : -1);
return;
}
break;
case 'Enter':
case 'Tab': {
if (hasSuggestions) {
event.preventDefault();
return typeaheadRef.insertSuggestion();
}
break;
}
default: {
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
break;
}
}
return next();
},
commands: {
selectSuggestion: (editor: CoreEditor, suggestion: CompletionItem): CoreEditor => {
const suggestions = state.groupedItems;
if (!suggestions || !suggestions.length) {
return editor;
}
// @ts-ignore
const ed = editor.applyTypeahead(suggestion);
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
return ed;
},
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) => (typeaheadRef = el)}
origin={portalOrigin}
prefix={state.typeaheadPrefix}
isOpen={!!state.groupedItems.length}
groupedItems={state.groupedItems}
onSelectSuggestion={(editor as CoreEditor & SuggestionsPluginCommands).selectSuggestion}
/>
</>
);
},
};
}
const handleTypeahead = async (
editor: CoreEditor,
onStateChange: (state: Partial<SuggestionsState>) => void,
onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>,
cleanText?: (text: string) => string
): Promise<void> => {
if (!onTypeahead) {
return null;
}
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();
// Find the first label key to the left of the cursor
const labelKeyDec = decorations
.filter(decoration => {
return (
decoration.end.offset <= myOffset &&
decoration.type === TOKEN_MARK &&
decoration.data.get('className').includes('label-key')
);
})
.last();
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
onStateChange({
groupedItems: filteredSuggestions,
typeaheadPrefix: prefix,
typeaheadContext: context,
typeaheadText: text,
});
// Bogus edit to force re-render
editor.blur().focus();
};

View File

@@ -1,10 +1,10 @@
// Types
import { Unsubscribable } from 'rxjs';
import { Emitter } from 'app/core/core';
import { DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction, PanelData } from '@grafana/ui';
import { DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction, PanelData, HistoryItem } from '@grafana/ui';
import { LogLevel, TimeRange, LoadingState, AbsoluteTimeRange } from '@grafana/data';
import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode } from 'app/types/explore';
import { ExploreId, ExploreItemState, ExploreUIState, ExploreMode } from 'app/types/explore';
import { actionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
/** Higher order actions

View File

@@ -1,62 +0,0 @@
import { GrafanaTheme } from '@grafana/ui';
import { default as calculateSize } from 'calculate-size';
import { CompletionItemGroup, CompletionItem, CompletionItemKind } from 'app/types';
export const flattenGroupItems = (groupedItems: CompletionItemGroup[]): CompletionItem[] => {
return groupedItems.reduce((all, current) => {
const titleItem: CompletionItem = {
label: current.label,
kind: CompletionItemKind.GroupTitle,
};
return all.concat(titleItem, current.items);
}, []);
};
export const calculateLongestLabel = (allItems: CompletionItem[]): string => {
return allItems.reduce((longest, current) => {
return longest.length < current.label.length ? current.label : longest;
}, '');
};
export const calculateListSizes = (theme: GrafanaTheme, allItems: CompletionItem[], longestLabel: string) => {
const size = calculateSize(longestLabel, {
font: theme.typography.fontFamily.monospace,
fontSize: theme.typography.size.sm,
fontWeight: 'normal',
});
const listWidth = calculateListWidth(size.width, theme);
const itemHeight = calculateItemHeight(size.height, theme);
const listHeight = calculateListHeight(itemHeight, allItems);
return {
listWidth,
listHeight,
itemHeight,
};
};
export const calculateItemHeight = (longestLabelHeight: number, theme: GrafanaTheme) => {
const horizontalPadding = parseInt(theme.spacing.sm, 10) * 2;
const itemHeight = longestLabelHeight + horizontalPadding;
return itemHeight;
};
export const calculateListWidth = (longestLabelWidth: number, theme: GrafanaTheme) => {
const verticalPadding = parseInt(theme.spacing.sm, 10) + parseInt(theme.spacing.md, 10);
const maxWidth = 800;
const listWidth = Math.min(Math.max(longestLabelWidth + verticalPadding, 200), maxWidth);
return listWidth;
};
export const calculateListHeight = (itemHeight: number, allItems: CompletionItem[]) => {
const numberOfItemsToShow = Math.min(allItems.length, 10);
const minHeight = 100;
const totalHeight = numberOfItemsToShow * itemHeight;
const listHeight = Math.max(totalHeight, minHeight);
return listHeight;
};

View File

@@ -4,8 +4,7 @@ import React from 'react';
import { SlatePrism } from '@grafana/ui';
// dom also includes Element polyfills
import QueryField from 'app/features/explore/QueryField';
import { ExploreQueryFieldProps } from '@grafana/ui';
import { QueryField, ExploreQueryFieldProps } from '@grafana/ui';
import { ElasticDatasource } from '../datasource';
import { ElasticsearchOptions, ElasticsearchQuery } from '../types';

View File

@@ -1,9 +1,5 @@
import PluginPrism from 'app/features/explore/slate-plugins/prism';
import BracesPlugin from 'app/features/explore/slate-plugins/braces';
import ClearPlugin from 'app/features/explore/slate-plugins/clear';
import NewlinePlugin from 'app/features/explore/slate-plugins/newline';
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
import { BracesPlugin, ClearPlugin, RunnerPlugin, NewlinePlugin } from '@grafana/ui';
import Typeahead from './typeahead';
import { getKeybindingSrv, KeybindingSrv } from 'app/core/services/keybindingSrv';

View File

@@ -3,23 +3,18 @@ import React from 'react';
// @ts-ignore
import Cascader from 'rc-cascader';
import { SlatePrism } from '@grafana/ui';
import { SlatePrism, TypeaheadOutput, SuggestionsState, QueryField, TypeaheadInput, BracesPlugin } from '@grafana/ui';
// Components
import QueryField, { TypeaheadInput } 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 { ExploreQueryFieldProps, 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';
import LokiDatasource from '../datasource';
function getChooserText(hasSyntax: boolean, hasLogLabels: boolean) {

View File

@@ -3,10 +3,10 @@ import { Editor as SlateEditor } from 'slate';
import LanguageProvider, { LABEL_REFRESH_INTERVAL, LokiHistoryItem, rangeToParams } from './language_provider';
import { AbsoluteTimeRange } from '@grafana/data';
import { TypeaheadInput } from '@grafana/ui';
import { advanceTo, clear, advanceBy } from 'jest-date-mock';
import { beforeEach } from 'test/lib/common';
import { TypeaheadInput } from '../../../types';
import { makeMockLokiDatasource } from './mocks';
import LokiDatasource from './datasource';

View File

@@ -6,12 +6,12 @@ import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasour
import syntax from './syntax';
// Types
import { CompletionItem, 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 { CompletionItem, TypeaheadInput, TypeaheadOutput, LanguageProvider, HistoryItem } from '@grafana/ui';
const DEFAULT_KEYS = ['job', 'namespace'];
const EMPTY_SELECTOR = '{}';

View File

@@ -3,21 +3,19 @@ import React from 'react';
// @ts-ignore
import Cascader from 'rc-cascader';
import { SlatePrism } from '@grafana/ui';
import { Plugin } from 'slate';
import { SlatePrism, TypeaheadInput, TypeaheadOutput, QueryField, BracesPlugin, HistoryItem } from '@grafana/ui';
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 { PromQuery, PromContext, PromOptions } from '../types';
import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
import { ExploreQueryFieldProps, QueryHint, DOMUtil } from '@grafana/ui';
import { isDataFrame, toLegacyResponseData } from '@grafana/data';
import { SuggestionsState } from '@grafana/ui';
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';
@@ -114,7 +112,7 @@ interface PromQueryFieldState {
}
class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryFieldState> {
plugins: any[];
plugins: Plugin[];
languageProvider: PromQlLanguageProvider;
languageProviderInitializationPromise: CancelablePromise<any>;

View File

@@ -1,15 +1,14 @@
import _ from 'lodash';
import { dateTime } from '@grafana/data';
import {
CompletionItem,
CompletionItemGroup,
LanguageProvider,
TypeaheadInput,
TypeaheadOutput,
CompletionItemGroup,
LanguageProvider,
HistoryItem,
} from 'app/types/explore';
} from '@grafana/ui';
import { parseSelector, processLabels, processHistogramLabels } from './language_utils';
import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql';

View File

@@ -1,6 +1,4 @@
/* tslint:disable max-line-length */
import { CompletionItem } from 'app/types/explore';
import { CompletionItem } from '@grafana/ui';
export const RATE_RANGES: CompletionItem[] = [
{ label: '$__interval', sortText: '$__interval' },

View File

@@ -2,7 +2,7 @@ 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 { HistoryItem } from '@grafana/ui';
import { PromQuery } from '../types';
describe('Language completion provider', () => {

View File

@@ -8,6 +8,7 @@ import {
ExploreStartPageProps,
PanelData,
DataQueryRequest,
HistoryItem,
} from '@grafana/ui';
import {
@@ -23,100 +24,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
* this is also the text that is inserted when selecting
* this completion.
*/
label: string;
/**
* The kind of this completion item. An icon is chosen
* by the editor based on the kind.
*/
kind?: CompletionItemKind | 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.
*/
move?: number;
}
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.
*/
skipSort?: boolean;
}
export enum ExploreId {
left = 'left',
right = 'right',
@@ -308,36 +220,6 @@ export interface ExploreUrlState {
context?: string;
}
export interface HistoryItem<TQuery extends DataQuery = DataQuery> {
ts: number;
query: TQuery;
}
export abstract class LanguageProvider {
datasource: DataSourceApi;
request: (url: string, params?: any) => Promise<any>;
/**
* Returns startTask that resolves with a task list when main syntax is loaded.
* Task list consists of secondary promises that load more detailed language features.
*/
start: () => Promise<any[]>;
startTask?: Promise<any[]>;
}
export interface TypeaheadInput {
text: string;
prefix: string;
wrapperClasses: string[];
labelKey?: string;
value?: Value;
editor?: Editor;
}
export interface TypeaheadOutput {
context?: string;
suggestions: CompletionItemGroup[];
}
export interface QueryIntervals {
interval: string;
intervalMs: number;