Azure Monitor: replace monaco by slate with initial Kusto syntax

This commit is contained in:
Alexander Zobnin 2019-01-29 14:19:31 +03:00
parent 96ffa9d797
commit fefb2c2ba2
No known key found for this signature in database
GPG Key ID: E17E9ABACEFA59EB
19 changed files with 1108 additions and 830 deletions

View File

@ -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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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">

View File

@ -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() {

View File

@ -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',

View File

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

View File

@ -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"