diff --git a/package.json b/package.json index d0dab98da3a..0850d1bdf44 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,6 @@ "url": "http://github.com/grafana/grafana.git" }, "devDependencies": { - "@alexanderzobnin/monaco-kusto": "^0.2.3-rc.1", "@babel/core": "^7.1.2", "@babel/plugin-syntax-dynamic-import": "^7.0.0", "@babel/preset-env": "^7.1.0", @@ -99,7 +98,6 @@ "tslint-react": "^3.6.0", "typescript": "^3.0.3", "uglifyjs-webpack-plugin": "^1.2.7", - "vscode-languageserver-types": "^3.14.0", "webpack": "4.19.1", "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx new file mode 100644 index 00000000000..9a5162fd6e0 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -0,0 +1,348 @@ +import Plain from 'slate-plain-serializer'; + +import QueryField from './query_field'; +// import debounce from './utils/debounce'; +// import {getNextCharacter} from './utils/dom'; +import debounce from 'app/features/explore/utils/debounce'; +import { getNextCharacter } from 'app/features/explore/utils/dom'; + +import { FUNCTIONS, KEYWORDS } from './kusto'; +// import '../sass/editor.base.scss'; + + +const TYPEAHEAD_DELAY = 500; + +interface Suggestion { + text: string; + deleteBackwards?: number; + type?: string; +} + +interface SuggestionGroup { + label: string; + items: Suggestion[]; + prefixMatch?: boolean; + skipFilter?: boolean; +} + +const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); +const wrapText = text => ({ text }); + +export default class KustoQueryField extends QueryField { + fields: any; + events: any; + + constructor(props, context) { + super(props, context); + + this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY); + } + + componentDidMount() { + this.updateMenu(); + } + + onTypeahead = () => { + const selection = window.getSelection(); + if (selection.anchorNode) { + const wrapperNode = selection.anchorNode.parentElement; + if (wrapperNode === null) { + return; + } + const editorNode = wrapperNode.closest('.slate-query-field'); + if (!editorNode || this.state.value.isBlurred) { + // Not inside this editor + return; + } + + // DOM ranges + const range = selection.getRangeAt(0); + const text = selection.anchorNode.textContent; + if (text === null) { + return; + } + const offset = range.startOffset; + let prefix = cleanText(text.substr(0, offset)); + + // Model ranges + const modelOffset = this.state.value.anchorOffset; + const modelPrefix = this.state.value.anchorText.text.slice(0, modelOffset); + + // Determine candidates by context + let suggestionGroups: SuggestionGroup[] = []; + const wrapperClasses = wrapperNode.classList; + let typeaheadContext: string | null = null; + + if (wrapperClasses.contains('function-context')) { + typeaheadContext = 'context-function'; + if (this.fields) { + suggestionGroups = this._getFieldsSuggestions(); + } else { + this._fetchFields(); + return; + } + } else if (modelPrefix.match(/(facet\s$)/i)) { + typeaheadContext = 'context-facet'; + if (this.fields) { + suggestionGroups = this._getFieldsSuggestions(); + } else { + this._fetchFields(); + return; + } + } else if (modelPrefix.match(/(,\s*$)/)) { + typeaheadContext = 'context-multiple-fields'; + if (this.fields) { + suggestionGroups = this._getFieldsSuggestions(); + } else { + this._fetchFields(); + return; + } + } else if (modelPrefix.match(/(from\s$)/i)) { + typeaheadContext = 'context-from'; + if (this.events) { + suggestionGroups = this._getAfterFromSuggestions(); + } else { + this._fetchEvents(); + return; + } + } else if (modelPrefix.match(/(^select\s\w*$)/i)) { + typeaheadContext = 'context-select'; + if (this.fields) { + suggestionGroups = this._getAfterSelectSuggestions(); + } else { + this._fetchFields(); + return; + } + } else if ( + modelPrefix.match(/\)\s$/) || + modelPrefix.match(/SELECT ((?:\$?\w+\(?\w*\)?\,?\s*)+)([\)]\s|\b)/gi) + ) { + typeaheadContext = 'context-after-function'; + suggestionGroups = this._getAfterFunctionSuggestions(); + } else if (modelPrefix.match(/from\s\S+\s\w*$/i)) { + prefix = ''; + typeaheadContext = 'context-since'; + suggestionGroups = this._getAfterEventSuggestions(); + // } else if (modelPrefix.match(/\d+\s\w*$/)) { + // typeaheadContext = 'context-number'; + // suggestionGroups = this._getAfterNumberSuggestions(); + } else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) { + typeaheadContext = 'context-timeseries'; + suggestionGroups = this._getAfterAgoSuggestions(); + } else if (prefix && !wrapperClasses.contains('argument')) { + typeaheadContext = 'context-builtin'; + suggestionGroups = this._getKeywordSuggestions(); + } else if (Plain.serialize(this.state.value) === '') { + typeaheadContext = 'context-new'; + suggestionGroups = this._getInitialSuggestions(); + } + + let results = 0; + prefix = prefix.toLowerCase(); + const filteredSuggestions = suggestionGroups.map(group => { + if (group.items && prefix && !group.skipFilter) { + group.items = group.items.filter(c => c.text.length >= prefix.length); + if (group.prefixMatch) { + group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) === 0); + } else { + group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) > -1); + } + } + results += group.items.length; + return group; + }) + .filter(group => group.items.length > 0); + + // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); + + this.setState({ + typeaheadPrefix: prefix, + typeaheadContext, + typeaheadText: text, + suggestions: results > 0 ? filteredSuggestions : [], + }); + } + } + + applyTypeahead(change, suggestion) { + const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state; + let suggestionText = suggestion.text || suggestion; + const move = 0; + + // Modify suggestion based on context + + const nextChar = getNextCharacter(); + if (suggestion.type === 'function') { + if (!nextChar || nextChar !== '(') { + suggestionText += '('; + } + } else if (typeaheadContext === 'context-function') { + if (!nextChar || nextChar !== ')') { + suggestionText += ')'; + } + } else { + if (!nextChar || nextChar !== ' ') { + suggestionText += ' '; + } + } + + this.resetTypeahead(); + + // Remove the current, incomplete text and replace it with the selected suggestion + const backward = suggestion.deleteBackwards || typeaheadPrefix.length; + const text = cleanText(typeaheadText); + const suffixLength = text.length - typeaheadPrefix.length; + const offset = typeaheadText.indexOf(typeaheadPrefix); + const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText); + const forward = midWord ? suffixLength + offset : 0; + + return change + .deleteBackward(backward) + .deleteForward(forward) + .insertText(suggestionText) + .move(move) + .focus(); + } + + private _getFieldsSuggestions(): SuggestionGroup[] { + return [ + { + prefixMatch: true, + label: 'Fields', + items: this.fields.map(wrapText) + }, + { + prefixMatch: true, + label: 'Variables', + items: this.props.templateVariables.map(wrapText) + } + ]; + } + + private _getAfterFromSuggestions(): SuggestionGroup[] { + return [ + { + skipFilter: true, + label: 'Events', + items: this.events.map(wrapText) + }, + { + prefixMatch: true, + label: 'Variables', + items: this.props.templateVariables + .map(wrapText) + .map(suggestion => { + suggestion.deleteBackwards = 0; + return suggestion; + }) + } + ]; + } + + private _getAfterSelectSuggestions(): SuggestionGroup[] { + return [ + { + prefixMatch: true, + label: 'Fields', + items: this.fields.map(wrapText) + }, + { + prefixMatch: true, + label: 'Functions', + items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Variables', + items: this.props.templateVariables.map(wrapText) + } + ]; + } + + private _getAfterFunctionSuggestions(): SuggestionGroup[] { + return [{ + prefixMatch: true, + label: 'Keywords', + items: ['FROM'].map(wrapText) + }]; + } + + private _getAfterEventSuggestions(): SuggestionGroup[] { + return [ + { + skipFilter: true, + label: 'Keywords', + items: ['SINCE'].map(wrapText) + .map((suggestion: any) => { + suggestion.deleteBackwards = 0; + return suggestion; + }) + }, + { + skipFilter: true, + label: 'Macros', + items: ['$__timeFilter'].map(wrapText) + .map((suggestion: any) => { + suggestion.deleteBackwards = 0; + return suggestion; + }) + } + ]; + } + + // private _getAfterNumberSuggestions(): SuggestionGroup[] { + // return [{ + // prefixMatch: true, + // label: 'Duration', + // items: DURATION + // .map(d => `${d} AGO`) + // .map(wrapText) + // }]; + // } + + private _getAfterAgoSuggestions(): SuggestionGroup[] { + return [{ + prefixMatch: true, + label: 'Keywords', + items: ['TIMESERIES', 'COMPARE WITH', 'FACET'].map(wrapText) + }]; + } + + private _getKeywordSuggestions(): SuggestionGroup[] { + return [{ + prefixMatch: true, + label: 'Keywords', + items: KEYWORDS.map(wrapText) + }]; + } + + private _getInitialSuggestions(): SuggestionGroup[] { + // TODO: return datbase tables as an initial suggestion + return [{ + prefixMatch: true, + label: 'Keywords', + items: KEYWORDS.map(wrapText) + }]; + } + + private async _fetchEvents() { + const query = 'events'; + const result = await this.request(query); + + if (result === undefined) { + this.events = []; + } else { + this.events = result; + } + setTimeout(this.onTypeahead, 0); + } + + private async _fetchFields() { + const query = 'fields'; + const result = await this.request(query); + + this.fields = result || []; + + setTimeout(this.onTypeahead, 0); + } +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx new file mode 100644 index 00000000000..51c1ec6f9e4 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx @@ -0,0 +1,61 @@ +import KustoQueryField from './KustoQueryField'; +import Kusto from './kusto'; + +import React, {Component} from 'react'; +import coreModule from 'app/core/core_module'; + + + +class Editor extends Component { + constructor(props) { + super(props); + this.state = { + edited: false, + query: props.query || '', + }; + } + + onChangeQuery = value => { + const { index, change } = this.props; + const { query } = this.state; + const edited = query !== value; + this.setState({ edited, query: value }); + if (change) { + change(value, index); + } + }; + + onPressEnter = () => { + const { execute } = this.props; + if (execute) { + execute(); + } + }; + + render() { + const { request, variables } = this.props; + const { edited, query } = this.state; + + return ( +
+ +
+ ); + } +} + +coreModule.directive('kustoEditor', [ + 'reactDirective', + reactDirective => { + return reactDirective(Editor, ['change', 'database', 'execute', 'query', 'request', 'variables']); + }, +]); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts new file mode 100644 index 00000000000..ecae890770d --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts @@ -0,0 +1,88 @@ +export const FUNCTIONS = [ + { text: 'countof', display: 'countof()', hint: '' }, + { text: 'bin', display: 'bin()', hint: '' }, + { text: 'extentid', display: 'extentid()', hint: '' }, + { text: 'extract', display: 'extract()', hint: '' }, + { text: 'extractjson', display: 'extractjson()', hint: '' }, + { text: 'floor', display: 'floor()', hint: '' }, + { text: 'iif', display: 'iif()', hint: '' }, + { text: 'isnull', display: 'isnull()', hint: '' }, + { text: 'isnotnull', display: 'isnotnull()', hint: '' }, + { text: 'notnull', display: 'notnull()', hint: '' }, + { text: 'isempty', display: 'isempty()', hint: '' }, + { text: 'isnotempty', display: 'isnotempty()', hint: '' }, + { text: 'notempty', display: 'notempty()', hint: '' }, + { text: 'now', display: 'now()', hint: '' }, + { text: 're2', display: 're2()', hint: '' }, + { text: 'strcat', display: 'strcat()', hint: '' }, + { text: 'strlen', display: 'strlen()', hint: '' }, + { text: 'toupper', display: 'toupper()', hint: '' }, + { text: 'tostring', display: 'tostring()', hint: '' }, + { text: 'count', display: 'count()', hint: '' }, + { text: 'cnt', display: 'cnt()', hint: '' }, + { text: 'sum', display: 'sum()', hint: '' }, + { text: 'min', display: 'min()', hint: '' }, + { text: 'max', display: 'max()', hint: '' }, + { text: 'avg', display: 'avg()', hint: '' }, +]; + +export const KEYWORDS = [ + 'by', 'on', 'contains', 'notcontains', 'containscs', 'notcontainscs', 'startswith', 'has', 'matches', 'regex', 'true', + 'false', 'and', 'or', 'typeof', 'int', 'string', 'date', 'datetime', 'time', 'long', 'real', '​boolean', 'bool', + // add some more keywords + 'where', 'order' +]; + +// Kusto operators +// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~']; + +export const DURATION = [ + 'SECONDS', + 'MINUTES', + 'HOURS', + 'DAYS', + 'WEEKS', + 'MONTHS', + 'YEARS' +]; + +const tokenizer = { + comment: { + pattern: /(^|[^\\:])\/\/.*/, + lookbehind: true, + greedy: true, + }, + 'function-context': { + pattern: /[a-z0-9_]+\([^)]*\)?/i, + inside: {}, + }, + duration: { + pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'), + alias: 'number', + }, + builtin: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.text).join('|')})(?=\\s*\\()`, 'i'), + string: { + pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, + greedy: true, + }, + keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|\\*)\\b`, 'i'), + boolean: /\b(?:true|false)\b/, + number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i, + operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/, + punctuation: /[{};(),.:]/, + variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/ +}; + +tokenizer['function-context'].inside = { + argument: { + pattern: /[a-z0-9_]+(?=:)/i, + alias: 'symbol', + }, + duration: tokenizer.duration, + number: tokenizer.number, + builtin: tokenizer.builtin, + string: tokenizer.string, + variable: tokenizer.variable, +}; + +export default tokenizer; 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 new file mode 100644 index 00000000000..6e395cfb198 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -0,0 +1,333 @@ +import PluginPrism from './slate-plugins/prism'; +// import PluginPrism from 'slate-prism'; +// import Prism from 'prismjs'; + +import BracesPlugin from 'app/features/explore/slate-plugins/braces'; +import ClearPlugin from 'app/features/explore/slate-plugins/clear'; +// Custom plugins (new line on Enter and run on Shift+Enter) +import NewlinePlugin from './slate-plugins/newline'; +import RunnerPlugin from './slate-plugins/runner'; + +import Typeahead from './typeahead'; + +import { Block, Document, Text, Value } from 'slate'; +import { Editor } from 'slate-react'; +import Plain from 'slate-plain-serializer'; +import ReactDOM from 'react-dom'; +import React from 'react'; +import _ from 'lodash'; + + +function flattenSuggestions(s) { + return s ? s.reduce((acc, g) => acc.concat(g.items), []) : []; +} + +export const makeFragment = text => { + const lines = text.split('\n').map(line => + Block.create({ + type: 'paragraph', + nodes: [Text.create(line)], + }) + ); + + const fragment = Document.create({ + nodes: lines, + }); + return fragment; +}; + +export const getInitialValue = query => Value.create({ document: makeFragment(query) }); + +class Portal extends React.Component { + node: any; + + constructor(props) { + super(props); + const { index = 0, prefix = 'query' } = props; + this.node = document.createElement('div'); + this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`); + document.body.appendChild(this.node); + } + + componentWillUnmount() { + document.body.removeChild(this.node); + } + + render() { + return ReactDOM.createPortal(this.props.children, this.node); + } +} + +class QueryField extends React.Component { + menuEl: any; + plugins: any; + resetTimer: any; + + constructor(props, context) { + super(props, context); + + const { prismDefinition = {}, prismLanguage = 'kusto' } = props; + + this.plugins = [ + BracesPlugin(), + ClearPlugin(), + RunnerPlugin({ handler: props.onPressEnter }), + NewlinePlugin(), + PluginPrism({ definition: prismDefinition, language: prismLanguage }), + ]; + + this.state = { + labelKeys: {}, + labelValues: {}, + suggestions: [], + typeaheadIndex: 0, + typeaheadPrefix: '', + value: getInitialValue(props.initialQuery || ''), + }; + } + + componentDidMount() { + this.updateMenu(); + } + + componentWillUnmount() { + clearTimeout(this.resetTimer); + } + + componentDidUpdate() { + this.updateMenu(); + } + + onChange = ({ value }) => { + const changed = value.document !== this.state.value.document; + this.setState({ value }, () => { + if (changed) { + this.onChangeQuery(); + } + }); + + window.requestAnimationFrame(this.onTypeahead); + } + + request = (url?) => { + if (this.props.request) { + return this.props.request(url); + } + return fetch(url); + } + + onChangeQuery = () => { + // Send text change to parent + const { onQueryChange } = this.props; + if (onQueryChange) { + onQueryChange(Plain.serialize(this.state.value)); + } + } + + onKeyDown = (event, change) => { + const { typeaheadIndex, suggestions } = this.state; + + switch (event.key) { + case 'Escape': { + if (this.menuEl) { + event.preventDefault(); + event.stopPropagation(); + this.resetTypeahead(); + return true; + } + break; + } + + case ' ': { + if (event.ctrlKey) { + event.preventDefault(); + this.onTypeahead(); + return true; + } + break; + } + + case 'Tab': { + if (this.menuEl) { + // Dont blur input + event.preventDefault(); + if (!suggestions || suggestions.length === 0) { + return undefined; + } + + // Get the currently selected suggestion + const flattenedSuggestions = flattenSuggestions(suggestions); + const selected = Math.abs(typeaheadIndex); + const selectedIndex = selected % flattenedSuggestions.length || 0; + const suggestion = flattenedSuggestions[selectedIndex]; + + this.applyTypeahead(change, suggestion); + return true; + } + break; + } + + case 'ArrowDown': { + if (this.menuEl) { + // Select next suggestion + event.preventDefault(); + this.setState({ typeaheadIndex: typeaheadIndex + 1 }); + } + break; + } + + case 'ArrowUp': { + if (this.menuEl) { + // Select previous suggestion + event.preventDefault(); + this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) }); + } + break; + } + + default: { + // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key); + break; + } + } + return undefined; + } + + onTypeahead = (change?, item?) => { + return change || this.state.value.change(); + } + + applyTypeahead(change?, suggestion?): { value: object } { return { value: {} }; } + + resetTypeahead = () => { + this.setState({ + suggestions: [], + typeaheadIndex: 0, + typeaheadPrefix: '', + typeaheadContext: null, + }); + } + + handleBlur = () => { + const { onBlur } = this.props; + // If we dont wait here, menu clicks wont work because the menu + // will be gone. + this.resetTimer = setTimeout(this.resetTypeahead, 100); + if (onBlur) { + onBlur(); + } + } + + handleFocus = () => { + const { onFocus } = this.props; + if (onFocus) { + onFocus(); + } + } + + onClickItem = item => { + const { suggestions } = this.state; + if (!suggestions || suggestions.length === 0) { + return; + } + + // Get the currently selected suggestion + const flattenedSuggestions = flattenSuggestions(suggestions); + const suggestion = _.find( + flattenedSuggestions, + suggestion => suggestion.display === item || suggestion.text === item + ); + + // Manually triggering change + const change = this.applyTypeahead(this.state.value.change(), suggestion); + this.onChange(change); + } + + updateMenu = () => { + const { suggestions } = this.state; + const menu = this.menuEl; + const selection = window.getSelection(); + const node = selection.anchorNode; + + // No menu, nothing to do + if (!menu) { + return; + } + + // No suggestions or blur, remove menu + const hasSuggesstions = suggestions && suggestions.length > 0; + if (!hasSuggesstions) { + menu.removeAttribute('style'); + return; + } + + // Align menu overlay to editor node + if (node && node.parentElement) { + // Read from DOM + const rect = node.parentElement.getBoundingClientRect(); + const scrollX = window.scrollX; + const scrollY = window.scrollY; + + // Write DOM + requestAnimationFrame(() => { + menu.style.opacity = 1; + menu.style.top = `${rect.top + scrollY + rect.height + 4}px`; + menu.style.left = `${rect.left + scrollX - 2}px`; + }); + } + } + + menuRef = el => { + this.menuEl = el; + } + + renderMenu = () => { + const { portalPrefix } = this.props; + const { suggestions } = this.state; + const hasSuggesstions = suggestions && suggestions.length > 0; + if (!hasSuggesstions) { + return null; + } + + // Guard selectedIndex to be within the length of the suggestions + let selectedIndex = Math.max(this.state.typeaheadIndex, 0); + const flattenedSuggestions = flattenSuggestions(suggestions); + selectedIndex = selectedIndex % flattenedSuggestions.length || 0; + const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map( + i => (typeof i === 'object' ? i.text : i) + ); + + // Create typeahead in DOM root so we can later position it absolutely + return ( + + + + ); + } + + render() { + return ( +
+ {this.renderMenu()} + +
+ ); + } +} + +export default QueryField; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts new file mode 100644 index 00000000000..d484d93a542 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts @@ -0,0 +1,35 @@ +function getIndent(text) { + let offset = text.length - text.trimLeft().length; + if (offset) { + let indent = text[0]; + while (--offset) { + indent += text[0]; + } + return indent; + } + return ''; +} + +export default function NewlinePlugin() { + return { + onKeyDown(event, change) { + const { value } = change; + if (!value.isCollapsed) { + return undefined; + } + + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + + const { startBlock } = value; + const currentLineText = startBlock.text; + const indent = getIndent(currentLineText); + + return change + .splitBlock() + .insertText(indent) + .focus(); + } + }, + }; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx new file mode 100644 index 00000000000..7c1b2f86139 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx @@ -0,0 +1,123 @@ +import React from 'react'; +import Prism from 'prismjs'; + +const TOKEN_MARK = 'prism-token'; + +export function setPrismTokens(language, field, values, alias = 'variable') { + Prism.languages[language][field] = { + alias, + pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`), + }; +} + +/** + * Code-highlighting plugin based on Prism and + * https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js + * + * (Adapted to handle nested grammar definitions.) + */ + +export default function PrismPlugin({ definition, language }) { + if (definition) { + // Don't override exising modified definitions + Prism.languages[language] = Prism.languages[language] || definition; + } + + return { + /** + * Render a Slate mark with appropiate CSS class names + * + * @param {Object} props + * @return {Element} + */ + + renderMark(props) { + const { children, mark } = props; + // Only apply spans to marks identified by this plugin + if (mark.type !== TOKEN_MARK) { + return undefined; + } + const className = `token ${mark.data.get('types')}`; + return {children}; + }, + + /** + * Decorate code blocks with Prism.js highlighting. + * + * @param {Node} node + * @return {Array} + */ + + decorateNode(node) { + if (node.type !== 'paragraph') { + return []; + } + + const texts = node.getTexts().toArray(); + const tstring = texts.map(t => t.text).join('\n'); + const grammar = Prism.languages[language]; + const tokens = Prism.tokenize(tstring, grammar); + const decorations: any[] = []; + let startText = texts.shift(); + let endText = startText; + let startOffset = 0; + let endOffset = 0; + let start = 0; + + function processToken(token, acc?) { + // Accumulate token types down the tree + const types = `${acc || ''} ${token.type || ''} ${token.alias || ''}`; + + // Add mark for token node + if (typeof token === 'string' || typeof token.content === 'string') { + startText = endText; + startOffset = endOffset; + + const content = typeof token === 'string' ? token : token.content; + const newlines = content.split('\n').length - 1; + const length = content.length - newlines; + const end = start + length; + + let available = startText.text.length - startOffset; + let remaining = length; + + endOffset = startOffset + remaining; + + while (available < remaining) { + endText = texts.shift(); + remaining = length - available; + available = endText.text.length; + endOffset = remaining; + } + + // Inject marks from up the tree (acc) as well + if (typeof token !== 'string' || acc) { + const range = { + anchorKey: startText.key, + anchorOffset: startOffset, + focusKey: endText.key, + focusOffset: endOffset, + marks: [{ type: TOKEN_MARK, data: { types } }], + }; + + decorations.push(range); + } + + start = end; + } else if (token.content && token.content.length) { + // Tokens can be nested + for (const subToken of token.content) { + processToken(subToken, types); + } + } + } + + // Process top-level tokens + for (const token of tokens) { + processToken(token); + } + + return decorations; + }, + }; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts new file mode 100644 index 00000000000..068bd9f0ad1 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts @@ -0,0 +1,14 @@ +export default function RunnerPlugin({ handler }) { + return { + onKeyDown(event) { + // Handle enter + if (handler && event.key === 'Enter' && event.shiftKey) { + // Submit on Enter + event.preventDefault(); + handler(event); + return true; + } + return undefined; + }, + }; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/typeahead.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/typeahead.tsx new file mode 100644 index 00000000000..44fce7f8c7e --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/typeahead.tsx @@ -0,0 +1,77 @@ +import React from 'react'; + +function scrollIntoView(el) { + if (!el || !el.offsetParent) { + return; + } + const container = el.offsetParent; + if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) { + container.scrollTop = el.offsetTop - container.offsetTop; + } +} + +class TypeaheadItem extends React.PureComponent { + el: any; + componentDidUpdate(prevProps) { + if (this.props.isSelected && !prevProps.isSelected) { + scrollIntoView(this.el); + } + } + + getRef = el => { + this.el = el; + }; + + render() { + const { hint, isSelected, label, onClickItem } = this.props; + const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item'; + const onClick = () => onClickItem(label); + return ( +
  • + {label} + {hint && isSelected ?
    {hint}
    : null} +
  • + ); + } +} + +class TypeaheadGroup extends React.PureComponent { + render() { + const { items, label, selected, onClickItem } = this.props; + return ( +
  • +
    {label}
    +
      + {items.map(item => { + const text = typeof item === 'object' ? item.text : item; + const label = typeof item === 'object' ? item.display || item.text : item; + return ( + -1} + hint={item.hint} + label={label} + /> + ); + })} +
    +
  • + ); + } +} + +class Typeahead extends React.PureComponent { + render() { + const { groupedItems, menuRef, selectedItems, onClickItem } = this.props; + return ( +
      + {groupedItems.map(g => ( + + ))} +
    + ); + } +} + +export default Typeahead; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/__mocks__/kusto_monaco_editor.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/__mocks__/kusto_monaco_editor.ts deleted file mode 100644 index 3e481a2ed6e..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/__mocks__/kusto_monaco_editor.ts +++ /dev/null @@ -1 +0,0 @@ -Object.assign({}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.test.ts deleted file mode 100644 index fe1446d5d3c..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.test.ts +++ /dev/null @@ -1,219 +0,0 @@ -// tslint:disable-next-line:no-reference -/// - -import KustoCodeEditor from './kusto_code_editor'; -import _ from 'lodash'; - -describe('KustoCodeEditor', () => { - let editor; - - describe('getCompletionItems', () => { - let completionItems; - let lineContent; - let model; - - beforeEach(() => { - (global as any).monaco = { - languages: { - CompletionItemKind: { - Keyword: '', - }, - }, - }; - model = { - getLineCount: () => 3, - getValueInRange: () => 'atable/n' + lineContent, - getLineContent: () => lineContent, - }; - - const StandaloneMock = jest.fn(); - editor = new KustoCodeEditor(null, 'TimeGenerated', () => {}, {}); - editor.codeEditor = new StandaloneMock(); - }); - - describe('when no where clause and no | in model text', () => { - beforeEach(() => { - lineContent = ' '; - const position = { lineNumber: 2, column: 2 }; - completionItems = editor.getCompletionItems(model, position); - }); - - it('should not return any grafana macros', () => { - expect(completionItems.length).toBe(0); - }); - }); - - describe('when no where clause in model text', () => { - beforeEach(() => { - lineContent = '| '; - const position = { lineNumber: 2, column: 3 }; - completionItems = editor.getCompletionItems(model, position); - }); - - it('should return grafana macros for where and timefilter', () => { - expect(completionItems.length).toBe(1); - - expect(completionItems[0].label).toBe('where $__timeFilter(timeColumn)'); - expect(completionItems[0].insertText.value).toBe('where \\$__timeFilter(${0:TimeGenerated})'); - }); - }); - - describe('when on line with where clause', () => { - beforeEach(() => { - lineContent = '| where Test == 2 and '; - const position = { lineNumber: 2, column: 23 }; - completionItems = editor.getCompletionItems(model, position); - }); - - it('should return grafana macros and variables', () => { - expect(completionItems.length).toBe(4); - - expect(completionItems[0].label).toBe('$__timeFilter(timeColumn)'); - expect(completionItems[0].insertText.value).toBe('\\$__timeFilter(${0:TimeGenerated})'); - - expect(completionItems[1].label).toBe('$__from'); - expect(completionItems[1].insertText.value).toBe('\\$__from'); - - expect(completionItems[2].label).toBe('$__to'); - expect(completionItems[2].insertText.value).toBe('\\$__to'); - - expect(completionItems[3].label).toBe('$__interval'); - expect(completionItems[3].insertText.value).toBe('\\$__interval'); - }); - }); - }); - - describe('onDidChangeCursorSelection', () => { - const keyboardEvent = { - selection: { - startLineNumber: 4, - startColumn: 26, - endLineNumber: 4, - endColumn: 31, - selectionStartLineNumber: 4, - selectionStartColumn: 26, - positionLineNumber: 4, - positionColumn: 31, - }, - secondarySelections: [], - source: 'keyboard', - reason: 3, - }; - - const modelChangedEvent = { - selection: { - startLineNumber: 2, - startColumn: 1, - endLineNumber: 3, - endColumn: 3, - selectionStartLineNumber: 2, - selectionStartColumn: 1, - positionLineNumber: 3, - positionColumn: 3, - }, - secondarySelections: [], - source: 'modelChange', - reason: 2, - }; - - describe('suggestion trigger', () => { - let suggestionTriggered; - let lineContent = ''; - - beforeEach(() => { - (global as any).monaco = { - languages: { - CompletionItemKind: { - Keyword: '', - }, - }, - editor: { - CursorChangeReason: { - NotSet: 0, - ContentFlush: 1, - RecoverFromMarkers: 2, - Explicit: 3, - Paste: 4, - Undo: 5, - Redo: 6, - }, - }, - }; - const StandaloneMock = jest.fn(() => ({ - getModel: () => { - return { - getLineCount: () => 3, - getLineContent: () => lineContent, - }; - }, - })); - - editor = new KustoCodeEditor(null, 'TimeGenerated', () => {}, {}); - editor.codeEditor = new StandaloneMock(); - editor.triggerSuggestions = () => { - suggestionTriggered = true; - }; - }); - - describe('when model change event, reason is RecoverFromMarkers and there is a space after', () => { - beforeEach(() => { - suggestionTriggered = false; - lineContent = '| '; - editor.onDidChangeCursorSelection(modelChangedEvent); - }); - - it('should trigger suggestion', () => { - expect(suggestionTriggered).toBeTruthy(); - }); - }); - - describe('when not model change event', () => { - beforeEach(() => { - suggestionTriggered = false; - editor.onDidChangeCursorSelection(keyboardEvent); - }); - - it('should not trigger suggestion', () => { - expect(suggestionTriggered).toBeFalsy(); - }); - }); - - describe('when model change event but with incorrect reason', () => { - beforeEach(() => { - suggestionTriggered = false; - const modelChangedWithInvalidReason = _.cloneDeep(modelChangedEvent); - modelChangedWithInvalidReason.reason = 5; - editor.onDidChangeCursorSelection(modelChangedWithInvalidReason); - }); - - it('should not trigger suggestion', () => { - expect(suggestionTriggered).toBeFalsy(); - }); - }); - - describe('when model change event but with no space after', () => { - beforeEach(() => { - suggestionTriggered = false; - lineContent = '|'; - editor.onDidChangeCursorSelection(modelChangedEvent); - }); - - it('should not trigger suggestion', () => { - expect(suggestionTriggered).toBeFalsy(); - }); - }); - - describe('when model change event but with no space after', () => { - beforeEach(() => { - suggestionTriggered = false; - lineContent = '|'; - editor.onDidChangeCursorSelection(modelChangedEvent); - }); - - it('should not trigger suggestion', () => { - expect(suggestionTriggered).toBeFalsy(); - }); - }); - }); - }); -}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.ts deleted file mode 100644 index 859faab8b36..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.ts +++ /dev/null @@ -1,332 +0,0 @@ -// tslint:disable-next-line:no-reference -/// - -import _ from 'lodash'; - -export interface SuggestionController { - _model: any; -} - -export default class KustoCodeEditor { - codeEditor: monaco.editor.IStandaloneCodeEditor; - completionItemProvider: monaco.IDisposable; - signatureHelpProvider: monaco.IDisposable; - - splitWithNewLineRegex = /[^\n]+\n?|\n/g; - newLineRegex = /\r?\n/; - startsWithKustoPipeRegex = /^\|\s*/g; - kustoPipeRegexStrict = /^\|\s*$/g; - - constructor( - private containerDiv: any, - private defaultTimeField: string, - private getSchema: () => any, - private config: any - ) {} - - initMonaco(scope) { - const themeName = this.config.bootData.user.lightTheme ? 'grafana-light' : 'vs-dark'; - - monaco.editor.defineTheme('grafana-light', { - base: 'vs', - inherit: true, - rules: [ - { token: 'comment', foreground: '008000' }, - { token: 'variable.predefined', foreground: '800080' }, - { token: 'function', foreground: '0000FF' }, - { token: 'operator.sql', foreground: 'FF4500' }, - { token: 'string', foreground: 'B22222' }, - { token: 'operator.scss', foreground: '0000FF' }, - { token: 'variable', foreground: 'C71585' }, - { token: 'variable.parameter', foreground: '9932CC' }, - { token: '', foreground: '000000' }, - { token: 'type', foreground: '0000FF' }, - { token: 'tag', foreground: '0000FF' }, - { token: 'annotation', foreground: '2B91AF' }, - { token: 'keyword', foreground: '0000FF' }, - { token: 'number', foreground: '191970' }, - { token: 'annotation', foreground: '9400D3' }, - { token: 'invalid', background: 'cd3131' }, - ], - colors: { - 'textCodeBlock.background': '#FFFFFF', - }, - }); - - monaco.languages['kusto'].kustoDefaults.setLanguageSettings({ - includeControlCommands: true, - newlineAfterPipe: true, - useIntellisenseV2: false, - useSemanticColorization: true, - }); - - this.codeEditor = monaco.editor.create(this.containerDiv, { - value: scope.content || 'Write your query here', - language: 'kusto', - // language: 'go', - selectionHighlight: false, - theme: themeName, - folding: true, - lineNumbers: 'off', - lineHeight: 16, - suggestFontSize: 13, - dragAndDrop: false, - occurrencesHighlight: false, - minimap: { - enabled: false, - }, - renderIndentGuides: false, - wordWrap: 'on', - }); - this.codeEditor.layout(); - - if (monaco.editor.getModels().length === 1) { - this.completionItemProvider = monaco.languages.registerCompletionItemProvider('kusto', { - triggerCharacters: ['.', ' '], - provideCompletionItems: this.getCompletionItems.bind(this), - }); - - this.signatureHelpProvider = monaco.languages.registerSignatureHelpProvider('kusto', { - signatureHelpTriggerCharacters: ['(', ')'], - provideSignatureHelp: this.getSignatureHelp.bind(this), - }); - } - - this.codeEditor.createContextKey('readyToExecute', true); - - this.codeEditor.onDidChangeCursorSelection(event => { - this.onDidChangeCursorSelection(event); - }); - - this.getSchema().then(schema => { - if (!schema) { - return; - } - - monaco.languages['kusto'].getKustoWorker().then(workerAccessor => { - const model = this.codeEditor.getModel(); - if (!model) { - return; - } - - workerAccessor(model.uri).then(worker => { - const dbName = Object.keys(schema.Databases).length > 0 ? Object.keys(schema.Databases)[0] : ''; - worker.setSchemaFromShowSchema(schema, 'https://help.kusto.windows.net', dbName); - this.codeEditor.layout(); - }); - }); - }); - } - - setOnDidChangeModelContent(listener) { - this.codeEditor.onDidChangeModelContent(listener); - } - - disposeMonaco() { - if (this.completionItemProvider) { - try { - this.completionItemProvider.dispose(); - } catch (e) { - console.error('Failed to dispose the completion item provider.', e); - } - } - if (this.signatureHelpProvider) { - try { - this.signatureHelpProvider.dispose(); - } catch (e) { - console.error('Failed to dispose the signature help provider.', e); - } - } - if (this.codeEditor) { - try { - this.codeEditor.dispose(); - } catch (e) { - console.error('Failed to dispose the editor component.', e); - } - } - } - - addCommand(keybinding: number, commandFunc: monaco.editor.ICommandHandler) { - this.codeEditor.addCommand(keybinding, commandFunc, 'readyToExecute'); - } - - getValue() { - return this.codeEditor.getValue(); - } - - toSuggestionController(srv: monaco.editor.IEditorContribution): SuggestionController { - return srv as any; - } - - setEditorContent(value) { - this.codeEditor.setValue(value); - } - - getCompletionItems(model: monaco.editor.IReadOnlyModel, position: monaco.Position) { - const timeFilterDocs = - '##### Macro that uses the selected timerange in Grafana to filter the query.\n\n' + - '- `$__timeFilter()` -> Uses the ' + - this.defaultTimeField + - ' column\n\n' + - '- `$__timeFilter(datetimeColumn)` -> Uses the specified datetime column to build the query.'; - - const textUntilPosition = model.getValueInRange({ - startLineNumber: 1, - startColumn: 1, - endLineNumber: position.lineNumber, - endColumn: position.column, - }); - - if (!_.includes(textUntilPosition, '|')) { - return []; - } - - if (!_.includes(textUntilPosition.toLowerCase(), 'where')) { - return [ - { - label: 'where $__timeFilter(timeColumn)', - kind: monaco.languages.CompletionItemKind.Keyword, - insertText: { - value: 'where \\$__timeFilter(${0:' + this.defaultTimeField + '})', - }, - documentation: { - value: timeFilterDocs, - }, - }, - ]; - } - - if (_.includes(model.getLineContent(position.lineNumber).toLowerCase(), 'where')) { - return [ - { - label: '$__timeFilter(timeColumn)', - kind: monaco.languages.CompletionItemKind.Keyword, - insertText: { - value: '\\$__timeFilter(${0:' + this.defaultTimeField + '})', - }, - documentation: { - value: timeFilterDocs, - }, - }, - { - label: '$__from', - kind: monaco.languages.CompletionItemKind.Keyword, - insertText: { - value: `\\$__from`, - }, - documentation: { - value: - 'Built-in variable that returns the from value of the selected timerange in Grafana.\n\n' + - 'Example: `where ' + - this.defaultTimeField + - ' > $__from` ', - }, - }, - { - label: '$__to', - kind: monaco.languages.CompletionItemKind.Keyword, - insertText: { - value: `\\$__to`, - }, - documentation: { - value: - 'Built-in variable that returns the to value of the selected timerange in Grafana.\n\n' + - 'Example: `where ' + - this.defaultTimeField + - ' < $__to` ', - }, - }, - { - label: '$__interval', - kind: monaco.languages.CompletionItemKind.Keyword, - insertText: { - value: `\\$__interval`, - }, - documentation: { - value: - '##### Built-in variable that returns an automatic time grain suitable for the current timerange.\n\n' + - 'Used with the bin() function - `bin(' + - this.defaultTimeField + - ', $__interval)` \n\n' + - '[Grafana docs](http://docs.grafana.org/reference/templating/#the-interval-variable)', - }, - }, - ]; - } - - return []; - } - - getSignatureHelp(model: monaco.editor.IReadOnlyModel, position: monaco.Position, token: monaco.CancellationToken) { - const textUntilPosition = model.getValueInRange({ - startLineNumber: position.lineNumber, - startColumn: position.column - 14, - endLineNumber: position.lineNumber, - endColumn: position.column, - }); - - if (textUntilPosition !== '$__timeFilter(') { - return {} as monaco.languages.SignatureHelp; - } - - const signature: monaco.languages.SignatureHelp = { - activeParameter: 0, - activeSignature: 0, - signatures: [ - { - label: '$__timeFilter(timeColumn)', - parameters: [ - { - label: 'timeColumn', - documentation: - 'Default is ' + - this.defaultTimeField + - ' column. Datetime column to filter data using the selected date range. ', - }, - ], - }, - ], - }; - - return signature; - } - - onDidChangeCursorSelection(event) { - if (event.source !== 'modelChange' || event.reason !== monaco.editor.CursorChangeReason.RecoverFromMarkers) { - return; - } - const lastChar = this.getCharAt(event.selection.positionLineNumber, event.selection.positionColumn - 1); - - if (lastChar !== ' ') { - return; - } - - this.triggerSuggestions(); - } - - triggerSuggestions() { - const suggestController = this.codeEditor.getContribution('editor.contrib.suggestController'); - if (!suggestController) { - return; - } - - const convertedController = this.toSuggestionController(suggestController); - - convertedController._model.cancel(); - setTimeout(() => { - convertedController._model.trigger(true); - }, 10); - } - - getCharAt(lineNumber: number, column: number) { - const model = this.codeEditor.getModel(); - if (model.getLineCount() === 0 || model.getLineCount() < lineNumber) { - return ''; - } - const line = model.getLineContent(lineNumber); - if (line.length < column || column < 1) { - return ''; - } - return line[column - 1]; - } -} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_monaco_editor.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_monaco_editor.ts deleted file mode 100644 index 90bc333edf8..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_monaco_editor.ts +++ /dev/null @@ -1,105 +0,0 @@ -// tslint:disable-next-line:no-reference -// /// - -import angular from 'angular'; -import KustoCodeEditor from './kusto_code_editor'; -import config from 'app/core/config'; - -/** - * Load monaco code editor and its' dependencies as a separate webpack chunk. - */ -function importMonaco() { - return import( - /* webpackChunkName: "monaco" */ - './monaco-loader' - ).then(monaco => { - return monaco; - }).catch(error => { - console.error('An error occurred while loading monaco-kusto:\n', error); - }); -} - -const editorTemplate = `
    `; - -function link(scope, elem, attrs) { - const containerDiv = elem.find('#content')[0]; - - if (!(global as any).monaco) { - // (global as any).System.import(`./${scope.pluginBaseUrl}/lib/monaco.min.js`).then(() => { - importMonaco().then(() => { - setTimeout(() => { - initMonaco(containerDiv, scope); - }, 1); - }); - } else { - setTimeout(() => { - initMonaco(containerDiv, scope); - }, 1); - } - - containerDiv.onblur = () => { - scope.onChange(); - }; - - containerDiv.onkeydown = evt => { - if (evt.key === 'Escape') { - evt.stopPropagation(); - return true; - } - - return undefined; - }; - - function initMonaco(containerDiv, scope) { - const kustoCodeEditor = new KustoCodeEditor(containerDiv, scope.defaultTimeField, scope.getSchema, config); - - kustoCodeEditor.initMonaco(scope); - - /* tslint:disable:no-bitwise */ - kustoCodeEditor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Enter, () => { - const newValue = kustoCodeEditor.getValue(); - scope.content = newValue; - scope.onChange(); - }); - /* tslint:enable:no-bitwise */ - - // Sync with outer scope - update editor content if model has been changed from outside of directive. - scope.$watch('content', (newValue, oldValue) => { - const editorValue = kustoCodeEditor.getValue(); - if (newValue !== editorValue && newValue !== oldValue) { - scope.$$postDigest(() => { - kustoCodeEditor.setEditorContent(newValue); - }); - } - }); - - kustoCodeEditor.setOnDidChangeModelContent(() => { - scope.$apply(() => { - const newValue = kustoCodeEditor.getValue(); - scope.content = newValue; - }); - }); - - scope.$on('$destroy', () => { - kustoCodeEditor.disposeMonaco(); - }); - } -} - -/** @ngInject */ -export function kustoMonacoEditorDirective() { - return { - restrict: 'E', - template: editorTemplate, - scope: { - content: '=', - onChange: '&', - getSchema: '&', - defaultTimeField: '@', - pluginBaseUrl: '@', - }, - link: link, - }; -} - -angular.module('grafana.controllers').directive('kustoMonacoEditor', kustoMonacoEditorDirective); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/monaco-loader.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/monaco-loader.ts deleted file mode 100644 index 779dff02821..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/monaco-loader.ts +++ /dev/null @@ -1,21 +0,0 @@ -// tslint:disable:no-reference -// /// - -// (1) Desired editor features: -import "monaco-editor/esm/vs/editor/browser/controller/coreCommands.js"; -import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget.js'; -import 'monaco-editor/esm/vs/editor/contrib/contextmenu/contextmenu.js'; -import "monaco-editor/esm/vs/editor/contrib/find/findController.js"; -import 'monaco-editor/esm/vs/editor/contrib/folding/folding.js'; -import 'monaco-editor/esm/vs/editor/contrib/format/formatActions.js'; -import 'monaco-editor/esm/vs/editor/contrib/multicursor/multicursor.js'; -import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController.js'; -import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter.js'; -import 'monaco-editor/esm/vs/editor/standalone/browser/iPadShowKeyboard/iPadShowKeyboard.js'; -import "monaco-editor/esm/vs/editor/editor.api.js"; - -// (2) Desired languages: -import '@alexanderzobnin/monaco-kusto/release/webpack/bridge.min.js'; -import '@alexanderzobnin/monaco-kusto/release/webpack/Kusto.JavaScript.Client.min.js'; -import '@alexanderzobnin/monaco-kusto/release/webpack/Kusto.Language.Bridge.min.js'; -import '@alexanderzobnin/monaco-kusto/release/webpack/monaco.contribution.min.js'; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html index e50d71ce0d5..49f02ec8355 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html @@ -118,8 +118,20 @@ - + + +
    + +
    diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index 65d3b473a3b..fd42c172f11 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -2,7 +2,8 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; // import './css/query_editor.css'; import TimegrainConverter from './time_grain_converter'; -import './monaco/kusto_monaco_editor'; +// import './monaco/kusto_monaco_editor'; +import './editor/editor_component'; export interface ResultFormat { text: string; @@ -323,6 +324,18 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .catch(this.handleQueryCtrlError.bind(this)); } + onLogAnalyticsQueryChange = (nextQuery: string) => { + this.target.azureLogAnalytics.query = nextQuery; + } + + onLogAnalyticsQueryExecute = () => { + this.panelCtrl.refresh(); + } + + get templateVariables() { + return this.templateSrv.variables.map(t => '$' + t.name); + } + /* Application Insights Section */ getAppInsightsAutoInterval() { diff --git a/scripts/webpack/webpack.dev.js b/scripts/webpack/webpack.dev.js index 916c0302c00..7e103b32606 100644 --- a/scripts/webpack/webpack.dev.js +++ b/scripts/webpack/webpack.dev.js @@ -2,7 +2,6 @@ const merge = require('webpack-merge'); const common = require('./webpack.common.js'); -const monaco = require('./webpack.monaco.js'); const path = require('path'); const webpack = require('webpack'); const HtmlWebpackPlugin = require("html-webpack-plugin"); @@ -10,7 +9,7 @@ const CleanWebpackPlugin = require('clean-webpack-plugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); // const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; -module.exports = merge(common, monaco, { +module.exports = merge(common, { devtool: "cheap-module-source-map", mode: 'development', diff --git a/scripts/webpack/webpack.monaco.js b/scripts/webpack/webpack.monaco.js deleted file mode 100644 index 6a332b3ed7e..00000000000 --- a/scripts/webpack/webpack.monaco.js +++ /dev/null @@ -1,122 +0,0 @@ -const path = require('path'); -const webpack = require('webpack'); -const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); - -module.exports = { - // output: { - // filename: 'monaco.min.js', - // path: path.resolve(__dirname, 'dist'), - // libraryTarget: 'umd', - // library: 'monaco', - // globalObject: 'self' - // }, - entry: { - // monaco: './public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/monaco-loader.ts', - }, - output: { - // filename: 'monaco.min.js', - // chunkFilename: '[name].bundle.js', - globalObject: 'self', - }, - resolveLoader: { - alias: { - 'blob-url-loader': require.resolve('./loaders/blobUrl'), - 'compile-loader': require.resolve('./loaders/compile'), - }, - }, - module: { - rules: [ - { - test: /\.css$/, - use: [ 'style-loader', 'css-loader' ] - }, - // { - // // https://github.com/bridgedotnet/Bridge/issues/3097 - // test: /bridge\.js$/, - // loader: 'regexp-replace-loader', - // options: { - // match: { - // pattern: "globals\\.System\\s=\\s\\{\\};" - // }, - // replaceWith: "$& System = globals.System; " - // } - // }, - // { - // test: /Kusto\.JavaScript\.Client\.js$/, - // loader: 'regexp-replace-loader', - // options: { - // match: { - // pattern: '"use strict";' - // }, - // replaceWith: "$& System = globals.System; " - // } - // }, - // { - // test: /Kusto\.Language\.Bridge\.js$/, - // loader: 'regexp-replace-loader', - // options: { - // match: { - // pattern: '"use strict";' - // }, - // replaceWith: "$& System = globals.System; " - // } - // }, - // { - // test: /newtonsoft\.json\.js$/, - // loader: 'regexp-replace-loader', - // options: { - // match: { - // pattern: '"use strict";' - // }, - // replaceWith: "$& System = globals.System; " - // } - // }, - // { - // test: /monaco\.contribution\.js$/, - // loader: 'regexp-replace-loader', - // options: { - // match: { - // pattern: 'vs/language/kusto/kustoMode', - // flags: 'g' - // }, - // replaceWith: "./kustoMode" - // } - // }, - ] - }, - optimization: { - splitChunks: { - // chunks: 'all', - cacheGroups: { - // monacoContribution: { - // test: /(src)|(node_modules(?!\/@kusto))/, - // name: 'monaco.contribution', - // enforce: false, - // // chunks: 'all', - // }, - // bridge: { - // test: /bridge/, - // name: 'bridge', - // chunks: 'all', - // }, - // KustoJavaScriptClient: { - // test: /Kusto\.JavaScript\.Client/, - // name: 'kusto.javaScript.client', - // chunks: 'all', - // }, - // KustoLanguageBridge: { - // test: /Kusto\.Language\.Bridge/, - // name: 'kusto.language.bridge', - // chunks: 'all', - // }, - } - } - }, - plugins: [ - new webpack.IgnorePlugin(/^((fs)|(path)|(os)|(crypto)|(source-map-support))$/, /vs\/language\/typescript\/lib/), - // new webpack.optimize.LimitChunkCountPlugin({ - // maxChunks: 1, - // }), - // new UglifyJSPlugin() - ], -}; diff --git a/yarn.lock b/yarn.lock index f291a832593..8faa61b5f1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,14 +2,6 @@ # yarn lockfile v1 -"@alexanderzobnin/monaco-kusto@^0.2.3-rc.1": - version "0.2.3-rc.1" - resolved "https://registry.yarnpkg.com/@alexanderzobnin/monaco-kusto/-/monaco-kusto-0.2.3-rc.1.tgz#1e96eef584d6173f19670afb3f122947329a840f" - integrity sha512-bppmiGfH7iXL4AdaKV2No4WqUoWBvS4bDejSYO0VjaK8zrNPFz9QhmGhnASHvQdmexcoPEqqQScA3udA2bQ68A== - dependencies: - "@kusto/language-service" "0.0.22-alpha" - "@kusto/language-service-next" "0.0.25-alpha1" - "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.0.0-beta.35": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" @@ -662,16 +654,6 @@ version "0.8.2" resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-0.8.2.tgz#576ff7fb1230185b619a75d258cbc98f0867a8dc" -"@kusto/language-service-next@0.0.25-alpha1": - version "0.0.25-alpha1" - resolved "https://registry.yarnpkg.com/@kusto/language-service-next/-/language-service-next-0.0.25-alpha1.tgz#73977b0873c7c2a23ae0c2cc1fef95a68c723c09" - integrity sha512-xxdY+Ei+e/GuzWZYoyjQqOfuzwVPMfHJwPRcxOdcSq5XMt9oZS+ryVH66l+CBxdZDdxEfQD2evVTXLjOAck5Rg== - -"@kusto/language-service@0.0.22-alpha": - version "0.0.22-alpha" - resolved "https://registry.yarnpkg.com/@kusto/language-service/-/language-service-0.0.22-alpha.tgz#990bbfb82e8e8991c35a12aab00d890a05fff623" - integrity sha512-oYiakH2Lq4j7ghahAtqxC+nuOKybH03H1o3IWyB3p8Ll4WkYQOrV8GWpqEjPtMfsuOt3t5k55OzzwDWFaX2zlw== - "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" resolved "https://registry.yarnpkg.com/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz#524af240d1a360527b730475ecfa1344aa540dde" @@ -12979,11 +12961,6 @@ vm-browserify@0.0.4: dependencies: indexof "0.0.1" -vscode-languageserver-types@^3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz#d3b5952246d30e5241592b6dde8280e03942e743" - integrity sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A== - w3c-blob@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/w3c-blob/-/w3c-blob-0.0.1.tgz#b0cd352a1a50f515563420ffd5861f950f1d85b8"