From 3e8c00dad1e0b2db902f2120f9690a64680fd7ce Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 31 Oct 2019 08:27:01 +0000 Subject: [PATCH] Chore: Moves QueryField to @grafana/ui (#19678) Closes #19626 --- .../QueryField/QueryField.story.tsx | 12 ++ .../QueryField}/QueryField.test.tsx | 6 +- .../src/components/QueryField}/QueryField.tsx | 42 +++--- .../src/components/Typeahead}/Typeahead.tsx | 62 ++++----- .../components/Typeahead/TypeaheadInfo.tsx | 52 ++++++++ .../components/Typeahead}/TypeaheadItem.tsx | 7 +- packages/grafana-ui/src/components/index.ts | 1 + .../src}/slate-plugins/braces.test.tsx | 4 +- .../grafana-ui/src}/slate-plugins/braces.ts | 19 +-- .../src}/slate-plugins/clear.test.tsx | 4 +- .../grafana-ui/src}/slate-plugins/clear.ts | 9 +- .../src}/slate-plugins/clipboard.ts | 37 +++--- .../src}/slate-plugins/indentation.ts | 25 ++-- .../grafana-ui/src/slate-plugins/index.ts | 8 ++ .../grafana-ui/src}/slate-plugins/newline.ts | 9 +- .../src}/slate-plugins/runner.test.tsx | 4 +- .../grafana-ui/src/slate-plugins/runner.ts | 20 +++ .../src}/slate-plugins/selection_shortcuts.ts | 9 +- .../src}/slate-plugins/suggestions.tsx | 75 ++++++----- .../grafana-ui/src/themes/ThemeContext.tsx | 1 + packages/grafana-ui/src/types/completion.ts | 109 ++++++++++++++++ packages/grafana-ui/src/types/datasource.ts | 16 +++ packages/grafana-ui/src/types/index.ts | 7 +- .../grafana-ui/src}/utils/typeahead.ts | 7 +- public/app/core/utils/explore.ts | 13 +- public/app/features/explore/QueryRow.tsx | 4 +- public/app/features/explore/TypeaheadInfo.tsx | 63 --------- .../features/explore/slate-plugins/runner.ts | 17 --- .../app/features/explore/state/actionTypes.ts | 4 +- .../components/ElasticsearchQueryField.tsx | 3 +- .../editor/query_field.tsx | 6 +- .../loki/components/LokiQueryFieldForm.tsx | 7 +- .../datasource/loki/language_provider.test.ts | 2 +- .../datasource/loki/language_provider.ts | 2 +- .../prometheus/components/PromQueryField.tsx | 10 +- .../prometheus/language_provider.ts | 7 +- .../plugins/datasource/prometheus/promql.ts | 4 +- .../specs/language_provider.test.ts | 2 +- public/app/types/explore.ts | 120 +----------------- 39 files changed, 417 insertions(+), 392 deletions(-) create mode 100644 packages/grafana-ui/src/components/QueryField/QueryField.story.tsx rename {public/app/features/explore => packages/grafana-ui/src/components/QueryField}/QueryField.test.tsx (59%) rename {public/app/features/explore => packages/grafana-ui/src/components/QueryField}/QueryField.tsx (87%) rename {public/app/features/explore => packages/grafana-ui/src/components/Typeahead}/Typeahead.tsx (81%) create mode 100644 packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx rename {public/app/features/explore => packages/grafana-ui/src/components/Typeahead}/TypeaheadItem.tsx (89%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/braces.test.tsx (94%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/braces.ts (73%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/clear.test.tsx (94%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/clear.ts (68%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/clipboard.ts (50%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/indentation.ts (75%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/newline.ts (74%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/runner.test.tsx (93%) create mode 100644 packages/grafana-ui/src/slate-plugins/runner.ts rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/selection_shortcuts.ts (72%) rename {public/app/features/explore => packages/grafana-ui/src}/slate-plugins/suggestions.tsx (85%) create mode 100644 packages/grafana-ui/src/types/completion.ts rename {public/app/features/explore => packages/grafana-ui/src}/utils/typeahead.ts (95%) delete mode 100644 public/app/features/explore/TypeaheadInfo.tsx delete mode 100644 public/app/features/explore/slate-plugins/runner.ts diff --git a/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx new file mode 100644 index 00000000000..11080294636 --- /dev/null +++ b/packages/grafana-ui/src/components/QueryField/QueryField.story.tsx @@ -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 ; +}); diff --git a/public/app/features/explore/QueryField.test.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx similarity index 59% rename from public/app/features/explore/QueryField.test.tsx rename to packages/grafana-ui/src/components/QueryField/QueryField.test.tsx index 71cdce520ab..5e8303b01b2 100644 --- a/public/app/features/explore/QueryField.test.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.test.tsx @@ -4,17 +4,17 @@ import { QueryField } from './QueryField'; describe('', () => { it('should render with null initial value', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should render with empty initial value', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); it('should render with initial value', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.find('div').exists()).toBeTruthy(); }); }); diff --git a/public/app/features/explore/QueryField.tsx b/packages/grafana-ui/src/components/QueryField/QueryField.tsx similarity index 87% rename from public/app/features/explore/QueryField.tsx rename to packages/grafana-ui/src/components/QueryField/QueryField.tsx index 226f6ffee04..58c6fb3c4d1 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/packages/grafana-ui/src/components/QueryField/QueryField.tsx @@ -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; 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 { 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) { super(props, context); @@ -100,7 +91,6 @@ export class QueryField extends React.PureComponent
(this.editor = editor)} + ref={editor => (this.editor = editor!)} schema={SCHEMA} autoCorrect={false} readOnly={this.props.disabled} diff --git a/public/app/features/explore/Typeahead.tsx b/packages/grafana-ui/src/components/Typeahead/Typeahead.tsx similarity index 81% rename from public/app/features/explore/Typeahead.tsx rename to packages/grafana-ui/src/components/Typeahead/Typeahead.tsx index 412436585fe..10f5c3d8736 100644 --- a/public/app/features/explore/Typeahead.tsx +++ b/packages/grafana-ui/src/components/Typeahead/Typeahead.tsx @@ -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 { + static contextType = ThemeContext; + context!: React.ContextType; listRef = createRef(); - 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 { 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 { 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 { }; 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 { } 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 { } 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 { return ( 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 { {showDocumentation && ( - + )} ); } } -export const TypeaheadWithTheme = withTheme(Typeahead); - interface PortalProps { index?: number; isOpen: boolean; diff --git a/packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx new file mode 100644 index 00000000000..a5df6b6ce1c --- /dev/null +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadInfo.tsx @@ -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 = ({ 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 ( +
+ {label} +
+ {documentation} +
+ ); +}; diff --git a/public/app/features/explore/TypeaheadItem.tsx b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx similarity index 89% rename from public/app/features/explore/TypeaheadItem.tsx rename to packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx index e20a5758613..098f73aabd7 100644 --- a/public/app/features/explore/TypeaheadItem.tsx +++ b/packages/grafana-ui/src/components/Typeahead/TypeaheadItem.tsx @@ -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) => { +export const TypeaheadItem: React.FC = (props: Props) => { const theme = useContext(ThemeContext); const styles = getStyles(theme); diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index e529cf7ecc5..f313d0f597c 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -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'; diff --git a/public/app/features/explore/slate-plugins/braces.test.tsx b/packages/grafana-ui/src/slate-plugins/braces.test.tsx similarity index 94% rename from public/app/features/explore/slate-plugins/braces.test.tsx rename to packages/grafana-ui/src/slate-plugins/braces.test.tsx index a80f67c817f..a56c34d0485 100644 --- a/public/app/features/explore/slate-plugins/braces.test.tsx +++ b/packages/grafana-ui/src/slate-plugins/braces.test.tsx @@ -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', () => { diff --git a/public/app/features/explore/slate-plugins/braces.ts b/packages/grafana-ui/src/slate-plugins/braces.ts similarity index 73% rename from public/app/features/explore/slate-plugins/braces.ts rename to packages/grafana-ui/src/slate-plugins/braces.ts index 0eff1fa7e4f..2ae602b962d 100644 --- a/public/app/features/explore/slate-plugins/braces.ts +++ b/packages/grafana-ui/src/slate-plugins/braces.ts @@ -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) diff --git a/public/app/features/explore/slate-plugins/clear.test.tsx b/packages/grafana-ui/src/slate-plugins/clear.test.tsx similarity index 94% rename from public/app/features/explore/slate-plugins/clear.test.tsx rename to packages/grafana-ui/src/slate-plugins/clear.test.tsx index 4565827e385..d1643010806 100644 --- a/public/app/features/explore/slate-plugins/clear.test.tsx +++ b/packages/grafana-ui/src/slate-plugins/clear.test.tsx @@ -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(''); diff --git a/public/app/features/explore/slate-plugins/clear.ts b/packages/grafana-ui/src/slate-plugins/clear.ts similarity index 68% rename from public/app/features/explore/slate-plugins/clear.ts rename to packages/grafana-ui/src/slate-plugins/clear.ts index 83dcf2e27b7..c711f68aff1 100644 --- a/public/app/features/explore/slate-plugins/clear.ts +++ b/packages/grafana-ui/src/slate-plugins/clear.ts @@ -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; diff --git a/public/app/features/explore/slate-plugins/clipboard.ts b/packages/grafana-ui/src/slate-plugins/clipboard.ts similarity index 50% rename from public/app/features/explore/slate-plugins/clipboard.ts rename to packages/grafana-ui/src/slate-plugins/clipboard.ts index 79d277ec65a..f62be7d97e6 100644 --- a/public/app/features/explore/slate-plugins/clipboard.ts +++ b/packages/grafana-ui/src/slate-plugins/clipboard.ts @@ -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; diff --git a/public/app/features/explore/slate-plugins/indentation.ts b/packages/grafana-ui/src/slate-plugins/indentation.ts similarity index 75% rename from public/app/features/explore/slate-plugins/indentation.ts rename to packages/grafana-ui/src/slate-plugins/indentation.ts index d3f1ab154c3..b848ccfd82f 100644 --- a/public/app/features/explore/slate-plugins/indentation.ts +++ b/packages/grafana-ui/src/slate-plugins/indentation.ts @@ -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(); } diff --git a/packages/grafana-ui/src/slate-plugins/index.ts b/packages/grafana-ui/src/slate-plugins/index.ts index 8ef6ba65b8b..8e7ea693ce4 100644 --- a/packages/grafana-ui/src/slate-plugins/index.ts +++ b/packages/grafana-ui/src/slate-plugins/index.ts @@ -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'; diff --git a/public/app/features/explore/slate-plugins/newline.ts b/packages/grafana-ui/src/slate-plugins/newline.ts similarity index 74% rename from public/app/features/explore/slate-plugins/newline.ts rename to packages/grafana-ui/src/slate-plugins/newline.ts index c31d2a74b18..d039c7d4961 100644 --- a/public/app/features/explore/slate-plugins/newline.ts +++ b/packages/grafana-ui/src/slate-plugins/newline.ts @@ -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; diff --git a/public/app/features/explore/slate-plugins/runner.test.tsx b/packages/grafana-ui/src/slate-plugins/runner.test.tsx similarity index 93% rename from public/app/features/explore/slate-plugins/runner.test.tsx rename to packages/grafana-ui/src/slate-plugins/runner.test.tsx index 3604681e03a..e572ba2dc01 100644 --- a/public/app/features/explore/slate-plugins/runner.test.tsx +++ b/packages/grafana-ui/src/slate-plugins/runner.test.tsx @@ -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(''); diff --git a/packages/grafana-ui/src/slate-plugins/runner.ts b/packages/grafana-ui/src/slate-plugins/runner.ts new file mode 100644 index 00000000000..e00ee25b756 --- /dev/null +++ b/packages/grafana-ui/src/slate-plugins/runner.ts @@ -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(); + }, + }; +} diff --git a/public/app/features/explore/slate-plugins/selection_shortcuts.ts b/packages/grafana-ui/src/slate-plugins/selection_shortcuts.ts similarity index 72% rename from public/app/features/explore/slate-plugins/selection_shortcuts.ts rename to packages/grafana-ui/src/slate-plugins/selection_shortcuts.ts index fb30ab51aeb..23211f356ab 100644 --- a/public/app/features/explore/slate-plugins/selection_shortcuts.ts +++ b/packages/grafana-ui/src/slate-plugins/selection_shortcuts.ts @@ -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(); diff --git a/public/app/features/explore/slate-plugins/suggestions.tsx b/packages/grafana-ui/src/slate-plugins/suggestions.tsx similarity index 85% rename from public/app/features/explore/slate-plugins/suggestions.tsx rename to packages/grafana-ui/src/slate-plugins/suggestions.tsx index b4133942f85..96fd2e6360a 100644 --- a/public/app/features/explore/slate-plugins/suggestions.tsx +++ b/packages/grafana-ui/src/slate-plugins/suggestions.tsx @@ -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; + onTypeahead?: (typeahead: TypeaheadInput) => Promise; 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} - (typeaheadRef = el)} + (typeaheadRef = menu)} origin={portalOrigin} prefix={state.typeaheadPrefix} isOpen={!!state.groupedItems.length} @@ -217,7 +221,7 @@ const handleTypeahead = async ( cleanText?: (text: string) => string ): Promise => { 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 diff --git a/packages/grafana-ui/src/themes/ThemeContext.tsx b/packages/grafana-ui/src/themes/ThemeContext.tsx index ea9fcb1502f..1e18c09e631 100644 --- a/packages/grafana-ui/src/themes/ThemeContext.tsx +++ b/packages/grafana-ui/src/themes/ThemeContext.tsx @@ -9,6 +9,7 @@ type Subtract = Omit; // Use Grafana Dark theme by default export const ThemeContext = React.createContext(getTheme(GrafanaThemeType.Dark)); +ThemeContext.displayName = 'ThemeContext'; export const withTheme =

(Component: React.ComponentType

) => { const WithTheme: React.FunctionComponent> = props => { diff --git a/packages/grafana-ui/src/types/completion.ts b/packages/grafana-ui/src/types/completion.ts new file mode 100644 index 00000000000..e93162c511e --- /dev/null +++ b/packages/grafana-ui/src/types/completion.ts @@ -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; +} diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index dae57884912..31a78d46dec 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -549,3 +549,19 @@ export interface AnnotationQueryRequest { name: string; } & MoreOptions; } + +export interface HistoryItem { + ts: number; + query: TQuery; +} + +export abstract class LanguageProvider { + datasource!: DataSourceApi; + request!: (url: string, params?: any) => Promise; + /** + * 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; + startTask?: Promise; +} diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index a4e07453bca..f636f38041c 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -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 }; diff --git a/public/app/features/explore/utils/typeahead.ts b/packages/grafana-ui/src/utils/typeahead.ts similarity index 95% rename from public/app/features/explore/utils/typeahead.ts rename to packages/grafana-ui/src/utils/typeahead.ts index e501e2ab60a..b3491e0a2a7 100644 --- a/public/app/features/explore/utils/typeahead.ts +++ b/packages/grafana-ui/src/utils/typeahead.ts @@ -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()); }; export const calculateLongestLabel = (allItems: CompletionItem[]): string => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 2bb45b3d321..bcae86b757c 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -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'; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index d9fb09a8f40..61bb0112e09 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -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'; diff --git a/public/app/features/explore/TypeaheadInfo.tsx b/public/app/features/explore/TypeaheadInfo.tsx deleted file mode 100644 index 85edae36606..00000000000 --- a/public/app/features/explore/TypeaheadInfo.tsx +++ /dev/null @@ -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 { - 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 ( -

- {label} -
- {documentation} -
- ); - } -} diff --git a/public/app/features/explore/slate-plugins/runner.ts b/public/app/features/explore/slate-plugins/runner.ts deleted file mode 100644 index bb3a10f8759..00000000000 --- a/public/app/features/explore/slate-plugins/runner.ts +++ /dev/null @@ -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(); - }, - }; -} diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 180c7474f09..59c581acf29 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -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 diff --git a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx index 6c3b00933f1..473b12130ce 100644 --- a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx +++ b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx @@ -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'; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 2a701839ba3..fd05ef7172f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -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'; diff --git a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx index dcf430f624e..4188d5f64c9 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryFieldForm.tsx @@ -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) { diff --git a/public/app/plugins/datasource/loki/language_provider.test.ts b/public/app/plugins/datasource/loki/language_provider.test.ts index 0b8caa16f0a..4e27c22fc63 100644 --- a/public/app/plugins/datasource/loki/language_provider.test.ts +++ b/public/app/plugins/datasource/loki/language_provider.test.ts @@ -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'; diff --git a/public/app/plugins/datasource/loki/language_provider.ts b/public/app/plugins/datasource/loki/language_provider.ts index ac5f877f8d4..d9812c1a536 100644 --- a/public/app/plugins/datasource/loki/language_provider.ts +++ b/public/app/plugins/datasource/loki/language_provider.ts @@ -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 = '{}'; diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 65b5f784177..4d8442c8f62 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -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 { - plugins: any[]; + plugins: Plugin[]; languageProvider: PromQlLanguageProvider; languageProviderInitializationPromise: CancelablePromise; diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 018b32e4981..290621a5a13 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -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'; diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts index d827677ee2d..0bde2da9c2f 100644 --- a/public/app/plugins/datasource/prometheus/promql.ts +++ b/public/app/plugins/datasource/prometheus/promql.ts @@ -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' }, diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 9171201fc37..8095922b80a 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -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', () => { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 92ec9194606..8cf7c13b353 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -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 { - ts: number; - query: TQuery; -} - -export abstract class LanguageProvider { - datasource: DataSourceApi; - request: (url: string, params?: any) => Promise; - /** - * 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; - startTask?: Promise; -} - -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;