mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { QueryField } from './QueryField';
|
||||
|
||||
const QueryFieldStories = storiesOf('UI/QueryField', module);
|
||||
|
||||
QueryFieldStories.addDecorator(withCenteredStory);
|
||||
|
||||
QueryFieldStories.add('default', () => {
|
||||
return <QueryField portalOrigin="mock-origin" query="" />;
|
||||
});
|
||||
@@ -4,17 +4,17 @@ import { QueryField } from './QueryField';
|
||||
|
||||
describe('<QueryField />', () => {
|
||||
it('should render with null initial value', () => {
|
||||
const wrapper = shallow(<QueryField query={null} />);
|
||||
const wrapper = shallow(<QueryField query={null} onTypeahead={jest.fn()} portalOrigin="mock-origin" />);
|
||||
expect(wrapper.find('div').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with empty initial value', () => {
|
||||
const wrapper = shallow(<QueryField query="" />);
|
||||
const wrapper = shallow(<QueryField query="" onTypeahead={jest.fn()} portalOrigin="mock-origin" />);
|
||||
expect(wrapper.find('div').exists()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render with initial value', () => {
|
||||
const wrapper = shallow(<QueryField query="my query" />);
|
||||
const wrapper = shallow(<QueryField query="my query" onTypeahead={jest.fn()} portalOrigin="mock-origin" />);
|
||||
expect(wrapper.find('div').exists()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -6,16 +6,17 @@ 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,
|
||||
NewlinePlugin,
|
||||
SelectionShortcutsPlugin,
|
||||
IndentationPlugin,
|
||||
ClipboardPlugin,
|
||||
RunnerPlugin,
|
||||
SuggestionsPlugin,
|
||||
} from '../../slate-plugins';
|
||||
|
||||
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';
|
||||
import { makeValue, SCHEMA, CompletionItemGroup, TypeaheadOutput, TypeaheadInput, SuggestionsState } from '../..';
|
||||
|
||||
export interface QueryFieldProps {
|
||||
additionalPlugins?: Plugin[];
|
||||
@@ -30,7 +31,7 @@ export interface QueryFieldProps {
|
||||
onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
|
||||
onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
|
||||
placeholder?: string;
|
||||
portalOrigin?: string;
|
||||
portalOrigin: string;
|
||||
syntax?: string;
|
||||
syntaxLoaded?: boolean;
|
||||
}
|
||||
@@ -43,15 +44,6 @@ export interface QueryFieldState {
|
||||
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.
|
||||
@@ -60,11 +52,10 @@ export interface TypeaheadInput {
|
||||
*/
|
||||
export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldState> {
|
||||
plugins: Plugin[];
|
||||
resetTimer: NodeJS.Timer;
|
||||
mounted: boolean;
|
||||
runOnChangeDebounced: Function;
|
||||
editor: Editor;
|
||||
lastExecutedValue: Value | null = null;
|
||||
mounted = false;
|
||||
editor: Editor | null = null;
|
||||
|
||||
constructor(props: QueryFieldProps, context: Context<any>) {
|
||||
super(props, context);
|
||||
@@ -100,7 +91,6 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
clearTimeout(this.resetTimer);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: QueryFieldProps, prevState: QueryFieldState) {
|
||||
@@ -119,6 +109,10 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps: QueryFieldProps) {
|
||||
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
|
||||
if (!this.editor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -196,7 +190,7 @@ export class QueryField extends React.PureComponent<QueryFieldProps, QueryFieldS
|
||||
<div className={wrapperClassName}>
|
||||
<div className="slate-query-field">
|
||||
<Editor
|
||||
ref={editor => (this.editor = editor)}
|
||||
ref={editor => (this.editor = editor!)}
|
||||
schema={SCHEMA}
|
||||
autoCorrect={false}
|
||||
readOnly={this.props.disabled}
|
||||
@@ -3,16 +3,15 @@ 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';
|
||||
import { TypeaheadItem } from './TypeaheadItem';
|
||||
import { flattenGroupItems, calculateLongestLabel, calculateListSizes } from '../../utils/typeahead';
|
||||
import { ThemeContext } from '../../themes/ThemeContext';
|
||||
import { CompletionItem, CompletionItemGroup, CompletionItemKind } from '../../types/completion';
|
||||
|
||||
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
|
||||
|
||||
interface Props extends Themeable {
|
||||
interface Props {
|
||||
origin: string;
|
||||
groupedItems: CompletionItemGroup[];
|
||||
prefix?: string;
|
||||
@@ -26,26 +25,33 @@ interface State {
|
||||
listWidth: number;
|
||||
listHeight: number;
|
||||
itemHeight: number;
|
||||
hoveredItem: number;
|
||||
hoveredItem: number | null;
|
||||
typeaheadIndex: number;
|
||||
}
|
||||
|
||||
export class Typeahead extends React.PureComponent<Props, State> {
|
||||
static contextType = ThemeContext;
|
||||
context!: React.ContextType<typeof ThemeContext>;
|
||||
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 };
|
||||
}
|
||||
state: State = { hoveredItem: null, typeaheadIndex: 1, allItems: [], listWidth: -1, listHeight: -1, itemHeight: -1 };
|
||||
|
||||
componentDidMount = () => {
|
||||
this.props.menuRef(this);
|
||||
if (this.props.menuRef) {
|
||||
this.props.menuRef(this);
|
||||
}
|
||||
|
||||
document.addEventListener('selectionchange', this.handleSelectionChange);
|
||||
|
||||
const allItems = flattenGroupItems(this.props.groupedItems);
|
||||
const longestLabel = calculateLongestLabel(allItems);
|
||||
const { listWidth, listHeight, itemHeight } = calculateListSizes(this.context, allItems, longestLabel);
|
||||
this.setState({
|
||||
listWidth,
|
||||
listHeight,
|
||||
itemHeight,
|
||||
allItems,
|
||||
});
|
||||
};
|
||||
|
||||
componentWillUnmount = () => {
|
||||
@@ -68,7 +74,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
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);
|
||||
const { listWidth, listHeight, itemHeight } = calculateListSizes(this.context, allItems, longestLabel);
|
||||
this.setState({ listWidth, listHeight, itemHeight, allItems });
|
||||
}
|
||||
};
|
||||
@@ -89,7 +95,6 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
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) {
|
||||
@@ -105,7 +110,9 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
insertSuggestion = () => {
|
||||
this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
|
||||
if (this.props.onSelectSuggestion) {
|
||||
this.props.onSelectSuggestion(this.state.allItems[this.state.typeaheadIndex]);
|
||||
}
|
||||
};
|
||||
|
||||
get menuPosition(): string {
|
||||
@@ -115,10 +122,10 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
const selection = window.getSelection();
|
||||
const node = selection.anchorNode;
|
||||
const node = selection && selection.anchorNode;
|
||||
|
||||
// Align menu overlay to editor node
|
||||
if (node) {
|
||||
if (node && node.parentElement) {
|
||||
// Read from DOM
|
||||
const rect = node.parentElement.getBoundingClientRect();
|
||||
const scrollX = window.scrollX;
|
||||
@@ -133,7 +140,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { prefix, theme, isOpen, origin } = this.props;
|
||||
const { prefix, isOpen = false, origin } = this.props;
|
||||
const { allItems, listWidth, listHeight, itemHeight, hoveredItem, typeaheadIndex } = this.state;
|
||||
|
||||
const showDocumentation = hoveredItem || typeaheadIndex;
|
||||
@@ -161,7 +168,7 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<TypeaheadItem
|
||||
onClickItem={() => this.props.onSelectSuggestion(item)}
|
||||
onClickItem={() => (this.props.onSelectSuggestion ? this.props.onSelectSuggestion(item) : {})}
|
||||
isSelected={allItems[typeaheadIndex] === item}
|
||||
item={item}
|
||||
prefix={prefix}
|
||||
@@ -175,20 +182,13 @@ export class Typeahead extends React.PureComponent<Props, State> {
|
||||
</ul>
|
||||
|
||||
{showDocumentation && (
|
||||
<TypeaheadInfo
|
||||
width={listWidth}
|
||||
height={listHeight}
|
||||
theme={theme}
|
||||
item={allItems[hoveredItem ? hoveredItem : typeaheadIndex]}
|
||||
/>
|
||||
<TypeaheadInfo height={listHeight} item={allItems[hoveredItem ? hoveredItem : typeaheadIndex]} />
|
||||
)}
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const TypeaheadWithTheme = withTheme(Typeahead);
|
||||
|
||||
interface PortalProps {
|
||||
index?: number;
|
||||
isOpen: boolean;
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
|
||||
import { CompletionItem, selectThemeVariant, ThemeContext, GrafanaTheme } from '../..';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme, height: number, visible: boolean) => {
|
||||
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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
||||
interface Props {
|
||||
item: CompletionItem;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const TypeaheadInfo: React.FC<Props> = ({ item, height }) => {
|
||||
const visible = item && !!item.documentation;
|
||||
const label = item ? item.label : '';
|
||||
const documentation = item && item.documentation ? item.documentation : '';
|
||||
const theme = useContext(ThemeContext);
|
||||
const styles = getStyles(theme, height, visible);
|
||||
|
||||
return (
|
||||
<div className={cx([styles.typeaheadItem])}>
|
||||
<b>{label}</b>
|
||||
<hr />
|
||||
<span>{documentation}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,10 @@
|
||||
import React, { FunctionComponent, useContext } from 'react';
|
||||
import React, { 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, CompletionItemKind, GrafanaTheme, ThemeContext, selectThemeVariant } from '../..';
|
||||
|
||||
interface Props {
|
||||
isSelected: boolean;
|
||||
@@ -57,7 +56,7 @@ const getStyles = (theme: GrafanaTheme) => ({
|
||||
`,
|
||||
});
|
||||
|
||||
export const TypeaheadItem: FunctionComponent<Props> = (props: Props) => {
|
||||
export const TypeaheadItem: React.FC<Props> = (props: Props) => {
|
||||
const theme = useContext(ThemeContext);
|
||||
const styles = getStyles(theme);
|
||||
|
||||
@@ -38,6 +38,7 @@ export { TimeOfDayPicker } from './TimePicker/TimeOfDayPicker';
|
||||
export { List } from './List/List';
|
||||
export { TagsInput } from './TagsInput/TagsInput';
|
||||
export { Modal } from './Modal/Modal';
|
||||
export { QueryField } from './QueryField/QueryField';
|
||||
|
||||
// Renderless
|
||||
export { SetInterval } from './SetInterval/SetInterval';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import Plain from 'slate-plain-serializer';
|
||||
import { Editor } from '@grafana/slate-react';
|
||||
import { shallow } from 'enzyme';
|
||||
import BracesPlugin from './braces';
|
||||
import { BracesPlugin } from './braces';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -11,7 +11,7 @@ declare global {
|
||||
}
|
||||
|
||||
describe('braces', () => {
|
||||
const handler = BracesPlugin().onKeyDown;
|
||||
const handler = BracesPlugin().onKeyDown!;
|
||||
const nextMock = () => {};
|
||||
|
||||
it('adds closing braces around empty value', () => {
|
||||
@@ -7,16 +7,17 @@ const BRACES: any = {
|
||||
'(': ')',
|
||||
};
|
||||
|
||||
export default function BracesPlugin(): Plugin {
|
||||
export function BracesPlugin(): Plugin {
|
||||
return {
|
||||
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
|
||||
onKeyDown(event: Event, editor: CoreEditor, next: Function) {
|
||||
const keyEvent = event as KeyboardEvent;
|
||||
const { value } = editor;
|
||||
|
||||
switch (event.key) {
|
||||
switch (keyEvent.key) {
|
||||
case '(':
|
||||
case '{':
|
||||
case '[': {
|
||||
event.preventDefault();
|
||||
keyEvent.preventDefault();
|
||||
const {
|
||||
start: { offset: startOffset, key: startKey },
|
||||
end: { offset: endOffset, key: endKey },
|
||||
@@ -27,17 +28,17 @@ export default function BracesPlugin(): Plugin {
|
||||
// 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])
|
||||
.insertTextByKey(startKey, startOffset, keyEvent.key)
|
||||
.insertTextByKey(endKey, endOffset + 1, BRACES[keyEvent.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);
|
||||
editor.insertText(`${keyEvent.key}${BRACES[keyEvent.key]}`).moveBackward(1);
|
||||
} else {
|
||||
editor.insertText(event.key);
|
||||
editor.insertText(keyEvent.key);
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -49,7 +50,7 @@ export default function BracesPlugin(): Plugin {
|
||||
const previousChar = text[offset - 1];
|
||||
const nextChar = text[offset];
|
||||
if (BRACES[previousChar] && BRACES[previousChar] === nextChar) {
|
||||
event.preventDefault();
|
||||
keyEvent.preventDefault();
|
||||
// Remove closing brace if directly following
|
||||
editor
|
||||
.deleteBackward(1)
|
||||
@@ -2,10 +2,10 @@ import Plain from 'slate-plain-serializer';
|
||||
import React from 'react';
|
||||
import { Editor } from '@grafana/slate-react';
|
||||
import { shallow } from 'enzyme';
|
||||
import ClearPlugin from './clear';
|
||||
import { ClearPlugin } from './clear';
|
||||
|
||||
describe('clear', () => {
|
||||
const handler = ClearPlugin().onKeyDown;
|
||||
const handler = ClearPlugin().onKeyDown!;
|
||||
|
||||
it('does not change the empty value', () => {
|
||||
const value = Plain.deserialize('');
|
||||
@@ -2,17 +2,18 @@ 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 function ClearPlugin(): Plugin {
|
||||
return {
|
||||
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
|
||||
onKeyDown(event: Event, editor: CoreEditor, next: Function) {
|
||||
const keyEvent = event as KeyboardEvent;
|
||||
const value = editor.value;
|
||||
|
||||
if (value.selection.isExpanded) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (event.key === 'k' && event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
if (keyEvent.key === 'k' && keyEvent.ctrlKey) {
|
||||
keyEvent.preventDefault();
|
||||
const text = value.anchorText.text;
|
||||
const offset = value.selection.anchor.offset;
|
||||
const length = text.length;
|
||||
@@ -10,10 +10,11 @@ const getCopiedText = (textBlocks: string[], startOffset: number, endOffset: num
|
||||
return textBlocks.join('\n').slice(startOffset, excludingLastLineLength + endOffset);
|
||||
};
|
||||
|
||||
export default function ClipboardPlugin(): Plugin {
|
||||
const clipboardPlugin = {
|
||||
onCopy(event: ClipboardEvent, editor: CoreEditor) {
|
||||
event.preventDefault();
|
||||
export function ClipboardPlugin(): Plugin {
|
||||
const clipboardPlugin: Plugin = {
|
||||
onCopy(event: Event, editor: CoreEditor, next: () => any) {
|
||||
const clipEvent = event as ClipboardEvent;
|
||||
clipEvent.preventDefault();
|
||||
|
||||
const { document, selection } = editor.value;
|
||||
const {
|
||||
@@ -26,22 +27,25 @@ export default function ClipboardPlugin(): Plugin {
|
||||
.map(block => block.text);
|
||||
|
||||
const copiedText = getCopiedText(selectedBlocks, startOffset, endOffset);
|
||||
if (copiedText) {
|
||||
event.clipboardData.setData('Text', copiedText);
|
||||
if (copiedText && clipEvent.clipboardData) {
|
||||
clipEvent.clipboardData.setData('Text', copiedText);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
onPaste(event: ClipboardEvent, editor: CoreEditor) {
|
||||
event.preventDefault();
|
||||
const pastedValue = event.clipboardData.getData('Text');
|
||||
const lines = pastedValue.split('\n');
|
||||
onPaste(event: Event, editor: CoreEditor, next: () => any) {
|
||||
const clipEvent = event as ClipboardEvent;
|
||||
clipEvent.preventDefault();
|
||||
if (clipEvent.clipboardData) {
|
||||
const pastedValue = clipEvent.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);
|
||||
if (lines.length) {
|
||||
editor.insertText(lines[0]);
|
||||
for (const line of lines.slice(1)) {
|
||||
editor.splitBlock().insertText(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,8 +55,9 @@ export default function ClipboardPlugin(): Plugin {
|
||||
|
||||
return {
|
||||
...clipboardPlugin,
|
||||
onCut(event: ClipboardEvent, editor: CoreEditor) {
|
||||
clipboardPlugin.onCopy(event, editor);
|
||||
onCut(event: Event, editor: CoreEditor, next: () => any) {
|
||||
const clipEvent = event as ClipboardEvent;
|
||||
clipboardPlugin.onCopy!(clipEvent, editor, next);
|
||||
editor.deleteAtRange(editor.value.selection);
|
||||
|
||||
return true;
|
||||
@@ -21,7 +21,7 @@ const handleTabKey = (event: KeyboardEvent, editor: CoreEditor, next: Function):
|
||||
const first = startBlock.getFirstText();
|
||||
|
||||
const startBlockIsSelected =
|
||||
startOffset === 0 && startKey === first.key && endOffset === first.text.length && endKey === first.key;
|
||||
first && startOffset === 0 && startKey === first.key && endOffset === first.text.length && endKey === first.key;
|
||||
|
||||
if (startBlockIsSelected || !startBlock.equals(endBlock)) {
|
||||
handleIndent(editor, 'right');
|
||||
@@ -38,7 +38,7 @@ const handleIndent = (editor: CoreEditor, indentDirection: 'left' | 'right') =>
|
||||
for (const block of selectedBlocks) {
|
||||
const blockWhitespace = block.text.length - block.text.trimLeft().length;
|
||||
|
||||
const textKey = block.getFirstText().key;
|
||||
const textKey = block.getFirstText()!.key;
|
||||
|
||||
const rangeProperties: RangeJSON = {
|
||||
anchor: {
|
||||
@@ -61,7 +61,7 @@ const handleIndent = (editor: CoreEditor, indentDirection: 'left' | 'right') =>
|
||||
const isWhiteSpace = /^\s*$/.test(textBeforeCaret);
|
||||
|
||||
for (const block of selectedBlocks) {
|
||||
editor.insertTextByKey(block.getFirstText().key, 0, SLATE_TAB);
|
||||
editor.insertTextByKey(block.getFirstText()!.key, 0, SLATE_TAB);
|
||||
}
|
||||
|
||||
if (isWhiteSpace) {
|
||||
@@ -71,18 +71,19 @@ const handleIndent = (editor: CoreEditor, indentDirection: 'left' | 'right') =>
|
||||
};
|
||||
|
||||
// Clears the rest of the line after the caret
|
||||
export default function IndentationPlugin(): Plugin {
|
||||
export function IndentationPlugin(): Plugin {
|
||||
return {
|
||||
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
|
||||
if (isIndentLeftHotkey(event) || isShiftTabHotkey(event)) {
|
||||
event.preventDefault();
|
||||
onKeyDown(event: Event, editor: CoreEditor, next: Function) {
|
||||
const keyEvent = event as KeyboardEvent;
|
||||
if (isIndentLeftHotkey(keyEvent) || isShiftTabHotkey(keyEvent)) {
|
||||
keyEvent.preventDefault();
|
||||
handleIndent(editor, 'left');
|
||||
} else if (isIndentRightHotkey(event)) {
|
||||
event.preventDefault();
|
||||
} else if (isIndentRightHotkey(keyEvent)) {
|
||||
keyEvent.preventDefault();
|
||||
handleIndent(editor, 'right');
|
||||
} else if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
handleTabKey(event, editor, next);
|
||||
} else if (keyEvent.key === 'Tab') {
|
||||
keyEvent.preventDefault();
|
||||
handleTabKey(keyEvent, editor, next);
|
||||
} else {
|
||||
return next();
|
||||
}
|
||||
@@ -1 +1,9 @@
|
||||
export { BracesPlugin } from './braces';
|
||||
export { ClearPlugin } from './clear';
|
||||
export { ClipboardPlugin } from './clipboard';
|
||||
export { IndentationPlugin } from './indentation';
|
||||
export { NewlinePlugin } from './newline';
|
||||
export { RunnerPlugin } from './runner';
|
||||
export { SelectionShortcutsPlugin } from './selection_shortcuts';
|
||||
export { SlatePrism } from './slate-prism';
|
||||
export { SuggestionsPlugin } from './suggestions';
|
||||
|
||||
@@ -13,17 +13,18 @@ function getIndent(text: string) {
|
||||
return '';
|
||||
}
|
||||
|
||||
export default function NewlinePlugin(): Plugin {
|
||||
export function NewlinePlugin(): Plugin {
|
||||
return {
|
||||
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
|
||||
onKeyDown(event: Event, editor: CoreEditor, next: Function) {
|
||||
const keyEvent = event as KeyboardEvent;
|
||||
const value = editor.value;
|
||||
|
||||
if (value.selection.isExpanded) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (event.key === 'Enter' && event.shiftKey) {
|
||||
event.preventDefault();
|
||||
if (keyEvent.key === 'Enter' && keyEvent.shiftKey) {
|
||||
keyEvent.preventDefault();
|
||||
|
||||
const { startBlock } = value;
|
||||
const currentLineText = startBlock.text;
|
||||
@@ -2,11 +2,11 @@ import Plain from 'slate-plain-serializer';
|
||||
import React from 'react';
|
||||
import { Editor } from '@grafana/slate-react';
|
||||
import { shallow } from 'enzyme';
|
||||
import RunnerPlugin from './runner';
|
||||
import { RunnerPlugin } from './runner';
|
||||
|
||||
describe('runner', () => {
|
||||
const mockHandler = jest.fn();
|
||||
const handler = RunnerPlugin({ handler: mockHandler }).onKeyDown;
|
||||
const handler = RunnerPlugin({ handler: mockHandler }).onKeyDown!;
|
||||
|
||||
it('should execute query when enter is pressed and there are no suggestions visible', () => {
|
||||
const value = Plain.deserialize('');
|
||||
20
packages/grafana-ui/src/slate-plugins/runner.ts
Normal file
20
packages/grafana-ui/src/slate-plugins/runner.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Plugin } from '@grafana/slate-react';
|
||||
import { Editor as CoreEditor } from 'slate';
|
||||
|
||||
export function RunnerPlugin({ handler }: any): Plugin {
|
||||
return {
|
||||
onKeyDown(event: Event, editor: CoreEditor, next: Function) {
|
||||
const keyEvent = event as KeyboardEvent;
|
||||
|
||||
// Handle enter
|
||||
if (handler && keyEvent.key === 'Enter' && !keyEvent.shiftKey) {
|
||||
// Submit on Enter
|
||||
keyEvent.preventDefault();
|
||||
handler(keyEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
return next();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -6,11 +6,12 @@ import { isKeyHotkey } from 'is-hotkey';
|
||||
const isSelectLineHotkey = isKeyHotkey('mod+l');
|
||||
|
||||
// Clears the rest of the line after the caret
|
||||
export default function SelectionShortcutsPlugin(): Plugin {
|
||||
export function SelectionShortcutsPlugin(): Plugin {
|
||||
return {
|
||||
onKeyDown(event: KeyboardEvent, editor: CoreEditor, next: Function) {
|
||||
if (isSelectLineHotkey(event)) {
|
||||
event.preventDefault();
|
||||
onKeyDown(event: Event, editor: CoreEditor, next: () => any) {
|
||||
const keyEvent = event as KeyboardEvent;
|
||||
if (isSelectLineHotkey(keyEvent)) {
|
||||
keyEvent.preventDefault();
|
||||
const { focusBlock, document } = editor.value;
|
||||
|
||||
editor.moveAnchorToStartOfBlock();
|
||||
@@ -4,14 +4,17 @@ 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';
|
||||
|
||||
import TOKEN_MARK from './slate-prism/TOKEN_MARK';
|
||||
import {
|
||||
makeFragment,
|
||||
TypeaheadOutput,
|
||||
CompletionItem,
|
||||
TypeaheadInput,
|
||||
SuggestionsState,
|
||||
CompletionItemGroup,
|
||||
} from '..';
|
||||
import { Typeahead } from '../components/Typeahead/Typeahead';
|
||||
export const TYPEAHEAD_DEBOUNCE = 100;
|
||||
|
||||
// Commands added to the editor by this plugin.
|
||||
@@ -27,13 +30,13 @@ export interface SuggestionsState {
|
||||
typeaheadText: string;
|
||||
}
|
||||
|
||||
export default function SuggestionsPlugin({
|
||||
export function SuggestionsPlugin({
|
||||
onTypeahead,
|
||||
cleanText,
|
||||
onWillApplySuggestion,
|
||||
portalOrigin,
|
||||
}: {
|
||||
onTypeahead: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
|
||||
onTypeahead?: (typeahead: TypeaheadInput) => Promise<TypeaheadOutput>;
|
||||
cleanText?: (text: string) => string;
|
||||
onWillApplySuggestion?: (suggestion: string, state: SuggestionsState) => string;
|
||||
portalOrigin: string;
|
||||
@@ -73,15 +76,16 @@ export default function SuggestionsPlugin({
|
||||
return next();
|
||||
},
|
||||
|
||||
onKeyDown: (event: KeyboardEvent, editor, next) => {
|
||||
onKeyDown: (event: Event, editor, next) => {
|
||||
const keyEvent = event as KeyboardEvent;
|
||||
const currentSuggestions = state.groupedItems;
|
||||
|
||||
const hasSuggestions = currentSuggestions.length;
|
||||
|
||||
switch (event.key) {
|
||||
switch (keyEvent.key) {
|
||||
case 'Escape': {
|
||||
if (hasSuggestions) {
|
||||
event.preventDefault();
|
||||
keyEvent.preventDefault();
|
||||
|
||||
state = {
|
||||
...state,
|
||||
@@ -98,8 +102,8 @@ export default function SuggestionsPlugin({
|
||||
case 'ArrowDown':
|
||||
case 'ArrowUp':
|
||||
if (hasSuggestions) {
|
||||
event.preventDefault();
|
||||
typeaheadRef.moveMenuIndex(event.key === 'ArrowDown' ? 1 : -1);
|
||||
keyEvent.preventDefault();
|
||||
typeaheadRef.moveMenuIndex(keyEvent.key === 'ArrowDown' ? 1 : -1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,7 +112,7 @@ export default function SuggestionsPlugin({
|
||||
case 'Enter':
|
||||
case 'Tab': {
|
||||
if (hasSuggestions) {
|
||||
event.preventDefault();
|
||||
keyEvent.preventDefault();
|
||||
return typeaheadRef.insertSuggestion();
|
||||
}
|
||||
|
||||
@@ -196,8 +200,8 @@ export default function SuggestionsPlugin({
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
<TypeaheadWithTheme
|
||||
menuRef={(el: Typeahead) => (typeaheadRef = el)}
|
||||
<Typeahead
|
||||
menuRef={(menu: Typeahead) => (typeaheadRef = menu)}
|
||||
origin={portalOrigin}
|
||||
prefix={state.typeaheadPrefix}
|
||||
isOpen={!!state.groupedItems.length}
|
||||
@@ -217,7 +221,7 @@ const handleTypeahead = async (
|
||||
cleanText?: (text: string) => string
|
||||
): Promise<void> => {
|
||||
if (!onTypeahead) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const { value } = editor;
|
||||
@@ -226,25 +230,28 @@ const handleTypeahead = async (
|
||||
// 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 decorations = parentBlock && parentBlock.getDecorations(editor as any);
|
||||
|
||||
const filteredDecorations = decorations
|
||||
.filter(
|
||||
decoration =>
|
||||
decoration.start.offset <= myOffset && decoration.end.offset > myOffset && decoration.type === TOKEN_MARK
|
||||
)
|
||||
.toArray();
|
||||
? 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 labelKeyDec =
|
||||
decorations &&
|
||||
decorations
|
||||
.filter(
|
||||
decoration =>
|
||||
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);
|
||||
|
||||
@@ -276,7 +283,7 @@ const handleTypeahead = async (
|
||||
text,
|
||||
value,
|
||||
wrapperClasses,
|
||||
labelKey,
|
||||
labelKey: labelKey || undefined,
|
||||
});
|
||||
|
||||
const filteredSuggestions = suggestions
|
||||
@@ -9,6 +9,7 @@ type Subtract<T, K> = Omit<T, keyof K>;
|
||||
|
||||
// Use Grafana Dark theme by default
|
||||
export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark));
|
||||
ThemeContext.displayName = 'ThemeContext';
|
||||
|
||||
export const withTheme = <P extends Themeable, S extends {} = {}>(Component: React.ComponentType<P>) => {
|
||||
const WithTheme: React.FunctionComponent<Subtract<P, Themeable>> = props => {
|
||||
|
||||
109
packages/grafana-ui/src/types/completion.ts
Normal file
109
packages/grafana-ui/src/types/completion.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Value } from 'slate';
|
||||
import { Editor } from '@grafana/slate-react';
|
||||
|
||||
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 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 TypeaheadOutput {
|
||||
context?: string;
|
||||
suggestions: CompletionItemGroup[];
|
||||
}
|
||||
|
||||
export interface TypeaheadInput {
|
||||
text: string;
|
||||
prefix: string;
|
||||
wrapperClasses: string[];
|
||||
labelKey?: string;
|
||||
value?: Value;
|
||||
editor?: Editor;
|
||||
}
|
||||
|
||||
export interface SuggestionsState {
|
||||
groupedItems: CompletionItemGroup[];
|
||||
typeaheadPrefix: string;
|
||||
typeaheadContext: string;
|
||||
typeaheadText: string;
|
||||
}
|
||||
@@ -549,3 +549,19 @@ export interface AnnotationQueryRequest<MoreOptions = {}> {
|
||||
name: string;
|
||||
} & MoreOptions;
|
||||
}
|
||||
|
||||
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[]>;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
export * from './app';
|
||||
export * from './completion';
|
||||
export * from './datasource';
|
||||
export * from './input';
|
||||
export * from './panel';
|
||||
export * from './plugin';
|
||||
export * from './app';
|
||||
export * from './datasource';
|
||||
export * from './theme';
|
||||
export * from './input';
|
||||
|
||||
import * as PanelEvents from './events';
|
||||
export { PanelEvents };
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { GrafanaTheme } from '@grafana/ui';
|
||||
import { default as calculateSize } from 'calculate-size';
|
||||
|
||||
import { CompletionItemGroup, CompletionItem, CompletionItemKind } from 'app/types';
|
||||
import { CompletionItemGroup, CompletionItem, CompletionItemKind } from '../types/completion';
|
||||
import { GrafanaTheme } from '..';
|
||||
|
||||
export const flattenGroupItems = (groupedItems: CompletionItemGroup[]): CompletionItem[] => {
|
||||
return groupedItems.reduce((all, current) => {
|
||||
@@ -10,7 +9,7 @@ export const flattenGroupItems = (groupedItems: CompletionItemGroup[]): Completi
|
||||
kind: CompletionItemKind.GroupTitle,
|
||||
};
|
||||
return all.concat(titleItem, current.items);
|
||||
}, []);
|
||||
}, new Array<CompletionItem>());
|
||||
};
|
||||
|
||||
export const calculateLongestLabel = (allItems: CompletionItem[]): string => {
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 = '{}';
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user