mirror of https://github.com/grafana/grafana
parent
96ffa9d797
commit
fefb2c2ba2
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
@ -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']); |
||||||
|
}, |
||||||
|
]); |
||||||
@ -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; |
||||||
@ -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(); |
||||||
|
} |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
@ -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; |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
@ -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; |
||||||
|
}, |
||||||
|
}; |
||||||
|
} |
||||||
@ -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; |
||||||
@ -1 +0,0 @@ |
|||||||
Object.assign({}); |
|
||||||
@ -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(); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
}); |
|
||||||
@ -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]; |
|
||||||
} |
|
||||||
} |
|
||||||
@ -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); |
|
||||||
@ -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'; |
|
||||||
@ -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()
|
|
||||||
], |
|
||||||
}; |
|
||||||
Loading…
Reference in new issue