Przeglądaj źródła

Azure Monitor: replace monaco by slate with initial Kusto syntax

Alexander Zobnin 7 lat temu
rodzic
commit
fefb2c2ba2
19 zmienionych plików z 1108 dodań i 830 usunięć
  1. 0 2
      package.json
  2. 348 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx
  3. 61 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx
  4. 88 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts
  5. 333 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx
  6. 35 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts
  7. 123 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx
  8. 14 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts
  9. 77 0
      public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/typeahead.tsx
  10. 0 1
      public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/__mocks__/kusto_monaco_editor.ts
  11. 0 219
      public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.test.ts
  12. 0 332
      public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.ts
  13. 0 105
      public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_monaco_editor.ts
  14. 0 21
      public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/monaco-loader.ts
  15. 14 2
      public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html
  16. 14 1
      public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts
  17. 1 2
      scripts/webpack/webpack.dev.js
  18. 0 122
      scripts/webpack/webpack.monaco.js
  19. 0 23
      yarn.lock

+ 0 - 2
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",

+ 348 - 0
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);
+  }
+}

+ 61 - 0
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<any, any> {
+  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 (
+      <div className="gf-form-input" style={{height: 'auto'}}>
+        <KustoQueryField
+          initialQuery={edited ? null : query}
+          onPressEnter={this.onPressEnter}
+          onQueryChange={this.onChangeQuery}
+          prismLanguage="kusto"
+          prismDefinition={Kusto}
+          placeholder="Enter a query"
+          request={request}
+          templateVariables={variables}
+        />
+      </div>
+    );
+  }
+}
+
+coreModule.directive('kustoEditor', [
+  'reactDirective',
+  reactDirective => {
+    return reactDirective(Editor, ['change', 'database', 'execute', 'query', 'request', 'variables']);
+  },
+]);

+ 88 - 0
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;

+ 333 - 0
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<any, any> {
+  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<any, any> {
+  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 (
+      <Portal prefix={portalPrefix}>
+        <Typeahead
+          menuRef={this.menuRef}
+          selectedItems={selectedKeys}
+          onClickItem={this.onClickItem}
+          groupedItems={suggestions}
+        />
+      </Portal>
+    );
+  }
+
+  render() {
+    return (
+      <div className="slate-query-field">
+        {this.renderMenu()}
+        <Editor
+          autoCorrect={false}
+          onBlur={this.handleBlur}
+          onKeyDown={this.onKeyDown}
+          onChange={this.onChange}
+          onFocus={this.handleFocus}
+          placeholder={this.props.placeholder}
+          plugins={this.plugins}
+          spellCheck={false}
+          value={this.state.value}
+        />
+      </div>
+    );
+  }
+}
+
+export default QueryField;

+ 35 - 0
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();
+      }
+    },
+  };
+}

+ 123 - 0
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 <span className={className}>{children}</span>;
+    },
+
+    /**
+     * 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;
+    },
+  };
+}

+ 14 - 0
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;
+    },
+  };
+}

+ 77 - 0
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<any, any> {
+  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 (
+      <li ref={this.getRef} className={className} onClick={onClick}>
+        {label}
+        {hint && isSelected ? <div className="typeahead-item-hint">{hint}</div> : null}
+      </li>
+    );
+  }
+}
+
+class TypeaheadGroup extends React.PureComponent<any, any> {
+  render() {
+    const { items, label, selected, onClickItem } = this.props;
+    return (
+      <li className="typeahead-group">
+        <div className="typeahead-group__title">{label}</div>
+        <ul className="typeahead-group__list">
+          {items.map(item => {
+            const text = typeof item === 'object' ? item.text : item;
+            const label = typeof item === 'object' ? item.display || item.text : item;
+            return (
+              <TypeaheadItem
+                key={text}
+                onClickItem={onClickItem}
+                isSelected={selected.indexOf(text) > -1}
+                hint={item.hint}
+                label={label}
+              />
+            );
+          })}
+        </ul>
+      </li>
+    );
+  }
+}
+
+class Typeahead extends React.PureComponent<any, any> {
+  render() {
+    const { groupedItems, menuRef, selectedItems, onClickItem } = this.props;
+    return (
+      <ul className="typeahead" ref={menuRef}>
+        {groupedItems.map(g => (
+          <TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItems} {...g} />
+        ))}
+      </ul>
+    );
+  }
+}
+
+export default Typeahead;

+ 0 - 1
public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/__mocks__/kusto_monaco_editor.ts

@@ -1 +0,0 @@
-Object.assign({});

+ 0 - 219
public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.test.ts

@@ -1,219 +0,0 @@
-// tslint:disable-next-line:no-reference
-///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
-
-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<monaco.editor.ICodeEditor>();
-      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<monaco.editor.ICodeEditor>(() => ({
-          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();
-        });
-      });
-    });
-  });
-});

+ 0 - 332
public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_code_editor.ts

@@ -1,332 +0,0 @@
-// tslint:disable-next-line:no-reference
-///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
-
-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];
-  }
-}

+ 0 - 105
public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/kusto_monaco_editor.ts

@@ -1,105 +0,0 @@
-// tslint:disable-next-line:no-reference
-// ///<reference path="../../../../../../node_modules/monaco-editor/monaco.d.ts" />
-
-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 = `<div id="content" tabindex="0" style="width: 100%; height: 120px"></div>`;
-
-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);

+ 0 - 21
public/app/plugins/datasource/grafana-azure-monitor-datasource/monaco/monaco-loader.ts

@@ -1,21 +0,0 @@
-// tslint:disable:no-reference
-// ///<reference path="../../../../../../node_modules/@alexanderzobnin/monaco-kusto/release/min/monaco.d.ts" />
-
-// (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';

+ 14 - 2
public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html

@@ -118,8 +118,20 @@
       </div>
     </div>
 
-    <kusto-monaco-editor content="ctrl.target.azureLogAnalytics.query" on-change="ctrl.refresh()" get-schema="ctrl.getAzureLogAnalyticsSchema()"
-      default-time-field="TimeGenerated" plugin-base-url={{ctrl.datasource.meta.baseUrl}}></kusto-monaco-editor>
+    <!-- <kusto-monaco-editor content="ctrl.target.azureLogAnalytics.query" on-change="ctrl.refresh()" get-schema="ctrl.getAzureLogAnalyticsSchema()"
+      default-time-field="TimeGenerated" plugin-base-url={{ctrl.datasource.meta.baseUrl}}></kusto-monaco-editor> -->
+
+    <div class="gf-form gf-form--grow">
+      <kusto-editor
+        class="gf-form gf-form--grow"
+        request="ctrl.requestMetadata"
+        style="border: none"
+        query="ctrl.target.azureLogAnalytics.query"
+        change="ctrl.onLogAnalyticsQueryChange"
+        execute="ctrl.onLogAnalyticsQueryExecute"
+        variables="ctrl.templateVariables"
+      />
+    </div>
 
     <div class="gf-form-inline">
       <div class="gf-form">

+ 14 - 1
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() {

+ 1 - 2
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',
 

+ 0 - 122
scripts/webpack/webpack.monaco.js

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

+ 0 - 23
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"