mirror of https://github.com/grafana/grafana
AzureMonitor: Remove unused editor component (#39874)
parent
6572017ec7
commit
42d7c32759
@ -1,83 +0,0 @@ |
||||
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; |
||||
|
||||
export class AzureMonitorAnnotationsQueryCtrl { |
||||
static templateUrl = 'partials/annotations.editor.html'; |
||||
declare datasource: any; |
||||
declare annotation: any; |
||||
declare workspaces: any[]; |
||||
declare subscriptions: Array<{ text: string; value: string }>; |
||||
private templateSrv: TemplateSrv = getTemplateSrv(); |
||||
|
||||
defaultQuery = |
||||
'<your table>\n| where $__timeFilter() \n| project TimeGenerated, Text=YourTitleColumn, Tags="tag1,tag2"'; |
||||
|
||||
/** @ngInject */ |
||||
constructor($scope: any) { |
||||
this.annotation = $scope.ctrl.annotation; |
||||
this.datasource = $scope.ctrl.datasource; |
||||
|
||||
this.annotation.queryType = this.annotation.queryType || 'Azure Log Analytics'; |
||||
this.annotation.rawQuery = this.annotation.rawQuery || this.defaultQuery; |
||||
this.initDropdowns(); |
||||
} |
||||
|
||||
async initDropdowns() { |
||||
await this.getSubscriptions(); |
||||
await this.getWorkspaces(); |
||||
} |
||||
|
||||
async getSubscriptions() { |
||||
if (!this.datasource.azureMonitorDatasource.isConfigured()) { |
||||
return; |
||||
} |
||||
|
||||
return this.datasource.azureMonitorDatasource.getSubscriptions().then((subs: any[]) => { |
||||
this.subscriptions = subs; |
||||
|
||||
if (!this.annotation.subscription && this.annotation.queryType === 'Azure Log Analytics') { |
||||
this.annotation.subscription = this.datasource.azureLogAnalyticsDatasource.subscriptionId; |
||||
} |
||||
|
||||
if (!this.annotation.subscription && this.subscriptions.length > 0) { |
||||
this.annotation.subscription = this.subscriptions[0].value; |
||||
} |
||||
}); |
||||
} |
||||
|
||||
async getWorkspaces(bustCache?: boolean) { |
||||
if (!bustCache && this.workspaces && this.workspaces.length > 0) { |
||||
return this.workspaces; |
||||
} |
||||
|
||||
return this.datasource |
||||
.getAzureLogAnalyticsWorkspaces(this.annotation.subscription) |
||||
.then((list: any[]) => { |
||||
this.workspaces = list; |
||||
if (list.length > 0 && !this.annotation.workspace) { |
||||
this.annotation.workspace = list[0].value; |
||||
} |
||||
return this.workspaces; |
||||
}) |
||||
.catch(() => {}); |
||||
} |
||||
|
||||
getAzureLogAnalyticsSchema = () => { |
||||
return this.getWorkspaces() |
||||
.then(() => { |
||||
return this.datasource.azureLogAnalyticsDatasource.getSchema(this.annotation.workspace); |
||||
}) |
||||
.catch(() => {}); |
||||
}; |
||||
|
||||
onSubscriptionChange = () => { |
||||
this.getWorkspaces(true); |
||||
}; |
||||
|
||||
onLogAnalyticsQueryChange = (nextQuery: string) => { |
||||
this.annotation.rawQuery = nextQuery; |
||||
}; |
||||
|
||||
get templateVariables() { |
||||
return this.templateSrv.getVariables().map((t: any) => '$' + t.name); |
||||
} |
||||
} |
||||
@ -1,445 +0,0 @@ |
||||
import { debounce, map } from 'lodash'; |
||||
import Plain from 'slate-plain-serializer'; |
||||
|
||||
import QueryField from './query_field'; |
||||
import { DOMUtil } from '@grafana/ui'; |
||||
import { Editor as CoreEditor } from 'slate'; |
||||
|
||||
import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto/kusto'; |
||||
// import '../sass/editor.base.scss';
|
||||
|
||||
const TYPEAHEAD_DELAY = 100; |
||||
|
||||
interface Suggestion { |
||||
text: string; |
||||
deleteBackwards?: number; |
||||
type?: string; |
||||
} |
||||
|
||||
interface SuggestionGroup { |
||||
label: string; |
||||
items: Suggestion[]; |
||||
prefixMatch?: boolean; |
||||
skipFilter?: boolean; |
||||
} |
||||
|
||||
interface KustoSchema { |
||||
Databases: { |
||||
Default: KustoDBSchema; |
||||
}; |
||||
Plugins?: any[]; |
||||
} |
||||
|
||||
interface KustoDBSchema { |
||||
Name?: string; |
||||
Functions?: any; |
||||
Tables?: any; |
||||
} |
||||
|
||||
const defaultSchema: any = () => ({ |
||||
Databases: { |
||||
Default: {}, |
||||
}, |
||||
}); |
||||
|
||||
const cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); |
||||
const wrapText = (text: string) => ({ text }); |
||||
|
||||
export default class KustoQueryField extends QueryField { |
||||
fields: any; |
||||
events: any; |
||||
schema: KustoSchema; |
||||
|
||||
constructor(props: any, context: any) { |
||||
super(props, context); |
||||
this.schema = defaultSchema(); |
||||
|
||||
this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY); |
||||
} |
||||
|
||||
componentDidMount() { |
||||
super.componentDidMount(); |
||||
this.fetchSchema(); |
||||
} |
||||
|
||||
onTypeahead = (force = false) => { |
||||
const selection = window.getSelection(); |
||||
|
||||
if (selection && 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; |
||||
|
||||
// Built-in functions
|
||||
if (wrapperClasses.contains('function-context')) { |
||||
typeaheadContext = 'context-function'; |
||||
suggestionGroups = this.getColumnSuggestions(); |
||||
|
||||
// where
|
||||
} else if (modelPrefix.match(/(where\s(\w+\b)?$)/i)) { |
||||
typeaheadContext = 'context-where'; |
||||
suggestionGroups = this.getColumnSuggestions(); |
||||
|
||||
// summarize by
|
||||
} else if (modelPrefix.match(/(summarize\s(\w+\b)?$)/i)) { |
||||
typeaheadContext = 'context-summarize'; |
||||
suggestionGroups = this.getFunctionSuggestions(); |
||||
} else if (modelPrefix.match(/(summarize\s(.+\s)?by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) { |
||||
typeaheadContext = 'context-summarize-by'; |
||||
suggestionGroups = this.getColumnSuggestions(); |
||||
|
||||
// order by, top X by, ... by ...
|
||||
} else if (modelPrefix.match(/(by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) { |
||||
typeaheadContext = 'context-by'; |
||||
suggestionGroups = this.getColumnSuggestions(); |
||||
|
||||
// join
|
||||
} else if (modelPrefix.match(/(on\s(.+\b)?$)/i)) { |
||||
typeaheadContext = 'context-join-on'; |
||||
suggestionGroups = this.getColumnSuggestions(); |
||||
} else if (modelPrefix.match(/(join\s+(\(\s+)?(\w+\b)?$)/i)) { |
||||
typeaheadContext = 'context-join'; |
||||
suggestionGroups = this.getTableSuggestions(); |
||||
|
||||
// distinct
|
||||
} else if (modelPrefix.match(/(distinct\s(.+\b)?$)/i)) { |
||||
typeaheadContext = 'context-distinct'; |
||||
suggestionGroups = this.getColumnSuggestions(); |
||||
|
||||
// database()
|
||||
} else if (modelPrefix.match(/(database\(\"(\w+)\"\)\.(.+\b)?$)/i)) { |
||||
typeaheadContext = 'context-database-table'; |
||||
const db = this.getDBFromDatabaseFunction(modelPrefix); |
||||
suggestionGroups = this.getTableSuggestions(db); |
||||
prefix = prefix.replace('.', ''); |
||||
|
||||
// new
|
||||
} else if (normalizeQuery(Plain.serialize(this.state.value)).match(/^\s*\w*$/i)) { |
||||
typeaheadContext = 'context-new'; |
||||
if (this.schema) { |
||||
suggestionGroups = this.getInitialSuggestions(); |
||||
} else { |
||||
this.fetchSchema(); |
||||
setTimeout(this.onTypeahead, 0); |
||||
return; |
||||
} |
||||
|
||||
// built-in
|
||||
} else if (prefix && !wrapperClasses.contains('argument') && !force) { |
||||
// Use only last typed word as a prefix for searching
|
||||
if (modelPrefix.match(/\s$/i)) { |
||||
prefix = ''; |
||||
return; |
||||
} |
||||
prefix = getLastWord(prefix); |
||||
typeaheadContext = 'context-builtin'; |
||||
suggestionGroups = this.getKeywordSuggestions(); |
||||
} else if (force === true) { |
||||
typeaheadContext = 'context-builtin-forced'; |
||||
if (modelPrefix.match(/\s$/i)) { |
||||
prefix = ''; |
||||
} |
||||
suggestionGroups = this.getKeywordSuggestions(); |
||||
} |
||||
|
||||
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);
|
||||
// console.log('onTypeahead', prefix, typeaheadContext, force);
|
||||
|
||||
this.setState({ |
||||
typeaheadPrefix: prefix, |
||||
typeaheadContext, |
||||
typeaheadText: text, |
||||
suggestions: results > 0 ? filteredSuggestions : [], |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
applyTypeahead = (editor: CoreEditor, suggestion: { text: any; type: string; deleteBackwards: any }): CoreEditor => { |
||||
const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state; |
||||
let suggestionText = suggestion.text || suggestion; |
||||
const move = 0; |
||||
|
||||
// Modify suggestion based on context
|
||||
|
||||
const nextChar = DOMUtil.getNextCharacter(); |
||||
if (suggestion.type === 'function') { |
||||
if (!nextChar || nextChar !== '(') { |
||||
suggestionText += '('; |
||||
} |
||||
} else if (typeaheadContext === 'context-function') { |
||||
if (!nextChar || nextChar !== ')') { |
||||
suggestionText += ')'; |
||||
} |
||||
} else { |
||||
if (!nextChar || nextChar !== ' ') { |
||||
suggestionText += ' '; |
||||
} |
||||
} |
||||
|
||||
// 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; |
||||
|
||||
this.resetTypeahead(() => |
||||
editor.deleteBackward(backward).deleteForward(forward).insertText(suggestionText).moveForward(move).focus() |
||||
); |
||||
|
||||
return editor; |
||||
}; |
||||
|
||||
// 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 getInitialSuggestions(): SuggestionGroup[] { |
||||
return this.getTableSuggestions(); |
||||
} |
||||
|
||||
private getKeywordSuggestions(): SuggestionGroup[] { |
||||
return [ |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Keywords', |
||||
items: KEYWORDS.map(wrapText), |
||||
}, |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Operators', |
||||
items: operatorTokens, |
||||
}, |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Functions', |
||||
items: functionTokens.map((s: any) => { |
||||
s.type = 'function'; |
||||
return s; |
||||
}), |
||||
}, |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Macros', |
||||
items: grafanaMacros.map((s: any) => { |
||||
s.type = 'function'; |
||||
return s; |
||||
}), |
||||
}, |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Tables', |
||||
items: map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })), |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
private getFunctionSuggestions(): SuggestionGroup[] { |
||||
return [ |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Functions', |
||||
items: functionTokens.map((s: any) => { |
||||
s.type = 'function'; |
||||
return s; |
||||
}), |
||||
}, |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Macros', |
||||
items: grafanaMacros.map((s: any) => { |
||||
s.type = 'function'; |
||||
return s; |
||||
}), |
||||
}, |
||||
]; |
||||
} |
||||
|
||||
getTableSuggestions(db = 'Default'): SuggestionGroup[] { |
||||
// @ts-ignore
|
||||
if (this.schema.Databases[db]) { |
||||
return [ |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Tables', |
||||
// @ts-ignore
|
||||
items: map(this.schema.Databases[db].Tables, (t: any) => ({ text: t.Name })), |
||||
}, |
||||
]; |
||||
} else { |
||||
return []; |
||||
} |
||||
} |
||||
|
||||
private getColumnSuggestions(): SuggestionGroup[] { |
||||
const table = this.getTableFromContext(); |
||||
if (table) { |
||||
const tableSchema = this.schema.Databases.Default.Tables[table]; |
||||
if (tableSchema) { |
||||
return [ |
||||
{ |
||||
prefixMatch: true, |
||||
label: 'Fields', |
||||
items: map(tableSchema.OrderedColumns, (f: any) => ({ |
||||
text: f.Name, |
||||
hint: f.Type, |
||||
})), |
||||
}, |
||||
]; |
||||
} |
||||
} |
||||
return []; |
||||
} |
||||
|
||||
private getTableFromContext() { |
||||
const query = Plain.serialize(this.state.value); |
||||
const tablePattern = /^\s*(\w+)\s*|/g; |
||||
const normalizedQuery = normalizeQuery(query); |
||||
const match = tablePattern.exec(normalizedQuery); |
||||
if (match && match.length > 1 && match[0] && match[1]) { |
||||
return match[1]; |
||||
} else { |
||||
return null; |
||||
} |
||||
} |
||||
|
||||
private getDBFromDatabaseFunction(prefix: string) { |
||||
const databasePattern = /database\(\"(\w+)\"\)/gi; |
||||
const match = databasePattern.exec(prefix); |
||||
if (match && match.length > 1 && match[0] && match[1]) { |
||||
return match[1]; |
||||
} else { |
||||
return undefined; |
||||
} |
||||
} |
||||
|
||||
private async fetchSchema() { |
||||
let schema = await this.props.getSchema(); |
||||
if (schema) { |
||||
if (schema.Type === 'AppInsights') { |
||||
schema = castSchema(schema); |
||||
} |
||||
this.schema = schema; |
||||
} else { |
||||
this.schema = defaultSchema(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Cast schema from App Insights to default Kusto schema |
||||
*/ |
||||
function castSchema(schema: any) { |
||||
const defaultSchemaTemplate = defaultSchema(); |
||||
defaultSchemaTemplate.Databases.Default = schema; |
||||
return defaultSchemaTemplate; |
||||
} |
||||
|
||||
function normalizeQuery(query: string): string { |
||||
const commentPattern = /\/\/.*$/gm; |
||||
let normalizedQuery = query.replace(commentPattern, ''); |
||||
normalizedQuery = normalizedQuery.replace('\n', ' '); |
||||
return normalizedQuery; |
||||
} |
||||
|
||||
function getLastWord(str: string): string { |
||||
const lastWordPattern = /(?:.*\s)?([^\s]+\s*)$/gi; |
||||
const match = lastWordPattern.exec(str); |
||||
if (match && match.length > 1) { |
||||
return match[1]; |
||||
} |
||||
return ''; |
||||
} |
||||
@ -1,81 +0,0 @@ |
||||
import KustoQueryField from './KustoQueryField'; |
||||
import Kusto from './kusto/kusto'; |
||||
|
||||
import React, { Component } from 'react'; |
||||
import coreModule from 'app/core/core_module'; |
||||
|
||||
interface EditorProps { |
||||
index: number; |
||||
placeholder?: string; |
||||
change: (value: string, index: number) => void; |
||||
variables: () => string[] | string[]; |
||||
getSchema?: () => Promise<any>; |
||||
execute?: () => void; |
||||
query?: string; |
||||
} |
||||
|
||||
class Editor extends Component<EditorProps, any> { |
||||
static defaultProps = { |
||||
placeholder: 'Enter a query', |
||||
}; |
||||
|
||||
constructor(props: EditorProps) { |
||||
super(props); |
||||
this.state = { |
||||
edited: false, |
||||
query: props.query || '', |
||||
}; |
||||
} |
||||
|
||||
onChangeQuery = (value: any) => { |
||||
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 { variables, getSchema, placeholder } = 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={placeholder} |
||||
templateVariables={variables} |
||||
getSchema={getSchema} |
||||
/> |
||||
</div> |
||||
); |
||||
} |
||||
} |
||||
|
||||
coreModule.directive('kustoEditor', [ |
||||
'reactDirective', |
||||
(reactDirective) => { |
||||
return reactDirective(Editor, [ |
||||
'change', |
||||
'database', |
||||
'execute', |
||||
'query', |
||||
'variables', |
||||
'placeholder', |
||||
['getSchema', { watchDepth: 'reference' }], |
||||
]); |
||||
}, |
||||
]); |
||||
@ -1,341 +0,0 @@ |
||||
import PluginPrism from 'app/features/explore/slate-plugins/prism'; |
||||
import { BracesPlugin, ClearPlugin, RunnerPlugin, NewlinePlugin } from '@grafana/ui'; |
||||
import Typeahead from './typeahead'; |
||||
import { keybindingSrv } from 'app/core/services/keybindingSrv'; |
||||
|
||||
import { Block, Document, Text, Value, Editor as CoreEditor } from 'slate'; |
||||
import { Editor } from '@grafana/slate-react'; |
||||
import Plain from 'slate-plain-serializer'; |
||||
import ReactDOM from 'react-dom'; |
||||
import React from 'react'; |
||||
|
||||
function flattenSuggestions(s: any) { |
||||
return s ? s.reduce((acc: any, g: any) => acc.concat(g.items), []) : []; |
||||
} |
||||
|
||||
export const makeFragment = (text: string) => { |
||||
const lines = text.split('\n').map((line: any) => |
||||
Block.create({ |
||||
type: 'paragraph', |
||||
nodes: [Text.create(line)], |
||||
} as any) |
||||
); |
||||
|
||||
const fragment = Document.create({ |
||||
nodes: lines, |
||||
}); |
||||
return fragment; |
||||
}; |
||||
|
||||
export const getInitialValue = (query: string) => Value.create({ document: makeFragment(query) }); |
||||
|
||||
class Portal extends React.Component<any, any> { |
||||
node: any; |
||||
|
||||
constructor(props: any) { |
||||
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: any, context: any) { |
||||
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: null, |
||||
typeaheadPrefix: '', |
||||
value: getInitialValue(props.initialQuery || ''), |
||||
}; |
||||
} |
||||
|
||||
componentDidMount() { |
||||
this.updateMenu(); |
||||
} |
||||
|
||||
componentWillUnmount() { |
||||
this.restoreEscapeKeyBinding(); |
||||
clearTimeout(this.resetTimer); |
||||
} |
||||
|
||||
componentDidUpdate() { |
||||
this.updateMenu(); |
||||
} |
||||
|
||||
onChange = ({ value }: { value: Value }) => { |
||||
const changed = value.document !== this.state.value.document; |
||||
this.setState({ value }, () => { |
||||
if (changed) { |
||||
// call typeahead only if query changed
|
||||
requestAnimationFrame(() => this.onTypeahead()); |
||||
this.onChangeQuery(); |
||||
} |
||||
}); |
||||
}; |
||||
|
||||
onChangeQuery = () => { |
||||
// Send text change to parent
|
||||
const { onQueryChange } = this.props; |
||||
if (onQueryChange) { |
||||
onQueryChange(Plain.serialize(this.state.value)); |
||||
} |
||||
}; |
||||
|
||||
onKeyDown = (event: Event, editor: CoreEditor, next: Function) => { |
||||
const { typeaheadIndex, suggestions } = this.state; |
||||
const keyboardEvent = event as KeyboardEvent; |
||||
|
||||
switch (keyboardEvent.key) { |
||||
case 'Escape': { |
||||
if (this.menuEl) { |
||||
keyboardEvent.preventDefault(); |
||||
keyboardEvent.stopPropagation(); |
||||
this.resetTypeahead(); |
||||
return true; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case ' ': { |
||||
if (keyboardEvent.ctrlKey) { |
||||
keyboardEvent.preventDefault(); |
||||
this.onTypeahead(true); |
||||
return true; |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 'Tab': |
||||
case 'Enter': { |
||||
if (this.menuEl && typeaheadIndex !== null) { |
||||
// Dont blur input
|
||||
keyboardEvent.preventDefault(); |
||||
if (!suggestions || !suggestions.length || keyboardEvent.shiftKey || keyboardEvent.ctrlKey) { |
||||
return next(); |
||||
} |
||||
|
||||
// Get the currently selected suggestion
|
||||
const flattenedSuggestions = flattenSuggestions(suggestions); |
||||
const selected = Math.abs(typeaheadIndex); |
||||
const selectedIndex = selected % flattenedSuggestions.length || 0; |
||||
const suggestion = flattenedSuggestions[selectedIndex]; |
||||
|
||||
return this.applyTypeahead(editor, suggestion); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 'ArrowDown': { |
||||
if (this.menuEl) { |
||||
// Select next suggestion
|
||||
keyboardEvent.preventDefault(); |
||||
this.setState({ typeaheadIndex: (typeaheadIndex || 0) + 1 }); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
case 'ArrowUp': { |
||||
if (this.menuEl) { |
||||
// Select previous suggestion
|
||||
keyboardEvent.preventDefault(); |
||||
this.setState({ typeaheadIndex: Math.max(0, (typeaheadIndex || 0) - 1) }); |
||||
} |
||||
break; |
||||
} |
||||
|
||||
default: { |
||||
// console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
|
||||
break; |
||||
} |
||||
} |
||||
return next(); |
||||
}; |
||||
|
||||
onTypeahead = (change = false, item?: any): boolean | void => { |
||||
return change; |
||||
}; |
||||
|
||||
applyTypeahead = ( |
||||
editor?: CoreEditor, |
||||
suggestion?: { text: any; type: string; deleteBackwards: any } |
||||
): { value: Value } => { |
||||
return { value: new Value() }; |
||||
}; |
||||
|
||||
resetTypeahead = (callback?: () => void) => { |
||||
this.setState( |
||||
{ |
||||
suggestions: [], |
||||
typeaheadIndex: null, |
||||
typeaheadPrefix: '', |
||||
typeaheadContext: null, |
||||
}, |
||||
callback |
||||
); |
||||
}; |
||||
|
||||
handleBlur = (event: Event, editor: CoreEditor, next: Function) => { |
||||
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(); |
||||
} |
||||
this.restoreEscapeKeyBinding(); |
||||
return next(); |
||||
}; |
||||
|
||||
handleFocus = (event: Event, editor: CoreEditor, next: Function) => { |
||||
const { onFocus } = this.props; |
||||
if (onFocus) { |
||||
onFocus(); |
||||
} |
||||
// Don't go back to dashboard if Escape pressed inside the editor.
|
||||
this.removeEscapeKeyBinding(); |
||||
return next(); |
||||
}; |
||||
|
||||
removeEscapeKeyBinding() { |
||||
keybindingSrv.unbind('esc', 'keydown'); |
||||
} |
||||
|
||||
restoreEscapeKeyBinding() { |
||||
keybindingSrv.initGlobals(); |
||||
} |
||||
|
||||
onClickItem = (item: any) => { |
||||
const { suggestions } = this.state; |
||||
if (!suggestions || suggestions.length === 0) { |
||||
return; |
||||
} |
||||
|
||||
// Manually triggering change
|
||||
const change = this.applyTypeahead(); |
||||
this.onChange(change); |
||||
}; |
||||
|
||||
updateMenu = () => { |
||||
const { suggestions } = this.state; |
||||
const menu = this.menuEl; |
||||
const selection = window.getSelection(); |
||||
|
||||
// No menu, nothing to do
|
||||
if (!menu || !selection) { |
||||
return; |
||||
} |
||||
|
||||
const node = selection.anchorNode; |
||||
|
||||
// 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; |
||||
const screenHeight = window.innerHeight; |
||||
|
||||
const menuLeft = rect.left + scrollX - 2; |
||||
const menuTop = rect.top + scrollY + rect.height + 4; |
||||
const menuHeight = screenHeight - menuTop - 10; |
||||
|
||||
// Write DOM
|
||||
requestAnimationFrame(() => { |
||||
menu.style.opacity = 1; |
||||
menu.style.top = `${menuTop}px`; |
||||
menu.style.left = `${menuLeft}px`; |
||||
menu.style.maxHeight = `${menuHeight}px`; |
||||
}); |
||||
} |
||||
}; |
||||
|
||||
menuRef = (el: any) => { |
||||
this.menuEl = el; |
||||
}; |
||||
|
||||
renderMenu = () => { |
||||
const { portalPrefix } = this.props; |
||||
const { suggestions, typeaheadIndex } = 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(typeaheadIndex, 0); |
||||
const flattenedSuggestions = flattenSuggestions(suggestions); |
||||
selectedIndex = selectedIndex % flattenedSuggestions.length || 0; |
||||
const selectedKeys = (typeaheadIndex !== null && 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; |
||||
@ -1,77 +0,0 @@ |
||||
import React from 'react'; |
||||
|
||||
function scrollIntoView(el: any) { |
||||
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: any) { |
||||
if (this.props.isSelected && !prevProps.isSelected) { |
||||
scrollIntoView(this.el); |
||||
} |
||||
} |
||||
|
||||
getRef = (el: any) => { |
||||
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: any) => { |
||||
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: any) => ( |
||||
<TypeaheadGroup key={g.label} onClickItem={onClickItem} selected={selectedItems} {...g} /> |
||||
))} |
||||
</ul> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export default Typeahead; |
||||
@ -1,531 +0,0 @@ |
||||
<query-editor-row |
||||
query-ctrl="ctrl" |
||||
can-collapse="false" |
||||
ng-if="!ctrl.reactQueryEditors.includes(ctrl.target.queryType)" |
||||
> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Service</label> |
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent"> |
||||
<select |
||||
class="gf-form-input service-dropdown min-width-12" |
||||
ng-model="ctrl.target.queryType" |
||||
ng-options="f.id as f.label for f in ctrl.queryQueryTypeOptions" |
||||
ng-change="ctrl.onQueryTypeChange()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div |
||||
class="gf-form" |
||||
ng-if="ctrl.target.queryType === 'Azure Monitor' || ctrl.target.queryType === 'Azure Log Analytics'" |
||||
> |
||||
<label class="gf-form-label query-keyword width-9">Subscription</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.subscription" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getSubscriptions()" |
||||
on-change="ctrl.onSubscriptionChange()" |
||||
css-class="min-width-12" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
<div ng-if="ctrl.target.queryType === 'Azure Monitor'"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Resource Group</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.azureMonitor.resourceGroup" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getResourceGroups($query)" |
||||
on-change="ctrl.onResourceGroupChange()" |
||||
css-class="min-width-12" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Namespace</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.azureMonitor.metricDefinition" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getMetricDefinitions($query)" |
||||
on-change="ctrl.onMetricDefinitionChange()" |
||||
css-class="min-width-20" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Resource Name</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.azureMonitor.resourceName" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getResourceNames($query)" |
||||
on-change="ctrl.onResourceNameChange()" |
||||
css-class="min-width-12" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Metric Namespace</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.azureMonitor.metricNamespace" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getMetricNamespaces($query)" |
||||
on-change="ctrl.onMetricNamespacesChange()" |
||||
css-class="min-width-12" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Metric</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.azureMonitor.metricName" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getMetricNames($query)" |
||||
on-change="ctrl.onMetricNameChange()" |
||||
css-class="min-width-12" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Aggregation</label> |
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent"> |
||||
<select |
||||
class="gf-form-input width-11" |
||||
ng-model="ctrl.target.azureMonitor.aggregation" |
||||
ng-options="f as f for f in ctrl.target.azureMonitor.aggOptions" |
||||
ng-change="ctrl.refresh()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Time Grain</label> |
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper"> |
||||
<select |
||||
class="gf-form-input" |
||||
ng-model="ctrl.target.azureMonitor.timeGrain" |
||||
ng-options="f.value as f.text for f in ctrl.target.azureMonitor.timeGrains" |
||||
ng-change="ctrl.refresh()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form" ng-show="ctrl.target.azureMonitor.timeGrain.trim() === 'auto'"> |
||||
<label class="gf-form-label">Auto Interval</label> |
||||
<label class="gf-form-label">{{ctrl.getAzureMonitorAutoInterval()}}</label> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
<!-- NO Filters--> |
||||
<ng-container ng-if="ctrl.target.azureMonitor.dimensionFilters.length < 1"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Dimension</label> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<a ng-click="ctrl.azureMonitorAddDimensionFilter()" class="gf-form-label query-part" |
||||
><icon name="'plus'"></icon |
||||
></a> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
</ng-container> |
||||
|
||||
<!-- YES Filters--> |
||||
<ng-container ng-if="ctrl.target.azureMonitor.dimensionFilters.length > 0"> |
||||
<div ng-repeat="dim in ctrl.target.azureMonitor.dimensionFilters track by $index" class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Dimension</label> |
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent"> |
||||
<select |
||||
class="gf-form-input min-width-12" |
||||
ng-model="dim.dimension" |
||||
ng-options="f.value as f.text for f in ctrl.target.azureMonitor.dimensions" |
||||
ng-change="ctrl.refresh()" |
||||
></select> |
||||
</div> |
||||
<label class="gf-form-label query-keyword width-3">eq</label> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-17" |
||||
ng-model="dim.filter" |
||||
spellcheck="false" |
||||
placeholder="Anything (*)" |
||||
ng-blur="ctrl.refresh()" |
||||
/> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<a ng-click="ctrl.azureMonitorRemoveDimensionFilter($index)" class="gf-form-label query-part" |
||||
><icon name="'minus'"></icon |
||||
></a> |
||||
</div> |
||||
<div class="gf-form" ng-if="$last"> |
||||
<a ng-click="ctrl.azureMonitorAddDimensionFilter()" class="gf-form-label query-part" |
||||
><icon name="'plus'"></icon |
||||
></a> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Top</label> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-3" |
||||
ng-model="ctrl.target.azureMonitor.top" |
||||
spellcheck="false" |
||||
placeholder="10" |
||||
ng-blur="ctrl.refresh()" |
||||
/> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
</ng-container> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Legend Format</label> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-30" |
||||
ng-model="ctrl.target.azureMonitor.alias" |
||||
spellcheck="false" |
||||
placeholder="alias patterns (see help for more info)" |
||||
ng-blur="ctrl.refresh()" |
||||
/> |
||||
</div> |
||||
|
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div ng-if="ctrl.target.queryType === 'Azure Log Analytics'"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Workspace</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.azureLogAnalytics.workspace" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getWorkspaces()" |
||||
on-change="ctrl.refresh()" |
||||
css-class="min-width-12" |
||||
> |
||||
</gf-form-dropdown> |
||||
<div class="gf-form"> |
||||
<div class="width-1"></div> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<button class="btn btn-primary width-10" ng-click="ctrl.refresh()">Run</button> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label">(Run Query: Shift+Enter, Trigger Suggestion: Ctrl+Space)</label> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form gf-form--grow"> |
||||
<kusto-editor |
||||
class="gf-form gf-form--grow" |
||||
query="ctrl.target.azureLogAnalytics.query" |
||||
change="ctrl.onLogAnalyticsQueryChange" |
||||
execute="ctrl.onLogAnalyticsQueryExecute" |
||||
variables="ctrl.templateVariables" |
||||
getSchema="ctrl.getAzureLogAnalyticsSchema" |
||||
/> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-7">Format As</label> |
||||
<div class="gf-form-select-wrapper"> |
||||
<select |
||||
class="gf-form-input gf-size-auto" |
||||
ng-model="ctrl.target.azureLogAnalytics.resultFormat" |
||||
ng-options="f.value as f.text for f in ctrl.resultFormats" |
||||
ng-change="ctrl.refresh()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showHelp = !ctrl.showHelp"> |
||||
Show Help |
||||
<icon name="'angle-down'" ng-show="ctrl.showHelp" style="margin-top: 3px"></icon> |
||||
<icon name="'angle-right'" ng-hide="ctrl.showHelp" style="margin-top: 3px"></icon> |
||||
</label> |
||||
</div> |
||||
<div class="gf-form" ng-show="ctrl.lastQuery"> |
||||
<label class="gf-form-label query-keyword" ng-click="ctrl.showLastQuery = !ctrl.showLastQuery"> |
||||
Raw Query |
||||
<icon name="'angle-down'" ng-show="ctrl.showLastQuery"></icon> |
||||
<icon name="'angle-right'" ng-hide="ctrl.showLastQuery"></icon> |
||||
</label> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form" ng-show="ctrl.showLastQuery"> |
||||
<pre class="gf-form-pre">{{ctrl.lastQuery}}</pre> |
||||
</div> |
||||
<div class="gf-form" ng-show="ctrl.showHelp"> |
||||
<pre class="gf-form-pre alert alert-info"> |
||||
Format as Table: |
||||
- return any set of columns |
||||
|
||||
Format as Time series: |
||||
- Requires a column of type datetime |
||||
- returns the first column with a numeric datatype as the value |
||||
- (Optional: returns the first column with type string to represent the series name. If no column is found the column name of the value column is used as series name) |
||||
|
||||
Example Time Series Query: |
||||
|
||||
AzureActivity |
||||
| where $__timeFilter() |
||||
| summarize count() by Category, bin(TimeGenerated, 60min) |
||||
| order by TimeGenerated asc |
||||
|
||||
Macros: |
||||
- $__timeFilter() -> TimeGenerated ≥ datetime(2018-06-05T18:09:58.907Z) and TimeGenerated ≤ datetime(2018-06-05T20:09:58.907Z) |
||||
- $__timeFilter(datetimeColumn) -> datetimeColumn ≥ datetime(2018-06-05T18:09:58.907Z) and datetimeColumn ≤ datetime(2018-06-05T20:09:58.907Z) |
||||
- $__escapeMulti($myTemplateVar) -> $myTemplateVar should be a multi-value template variables that contains illegal characters |
||||
- $__contains(aColumn, $myTemplateVar) -> aColumn in ($myTemplateVar) |
||||
If using the All option, then check the Include All Option checkbox and in the Custom all value field type in: all. If All is chosen -> 1 == 1 |
||||
|
||||
Or build your own conditionals using these built-in variables which just return the values: |
||||
- $__timeFrom -> datetime(2018-06-05T18:09:58.907Z) |
||||
- $__timeTo -> datetime(2018-06-05T20:09:58.907Z) |
||||
- $__interval -> 5m |
||||
|
||||
Examples: |
||||
- ¡ where $__timeFilter |
||||
- | where TimeGenerated ≥ $__timeFrom and TimeGenerated ≤ $__timeTo |
||||
- | summarize count() by Category, bin(TimeGenerated, $__interval) |
||||
</pre> |
||||
</div> |
||||
</div> |
||||
|
||||
<div ng-if="ctrl.target.queryType === 'Insights Analytics'"> |
||||
<div class="gf-form gf-form--grow"> |
||||
<kusto-editor |
||||
class="gf-form gf-form--grow" |
||||
query="ctrl.target.insightsAnalytics.query" |
||||
placeholder="'Application Insights Query'" |
||||
change="ctrl.onInsightsAnalyticsQueryChange" |
||||
execute="ctrl.onQueryExecute" |
||||
variables="ctrl.templateVariables" |
||||
getSchema="ctrl.getAppInsightsQuerySchema" |
||||
/> |
||||
</div> |
||||
|
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-7">Format As</label> |
||||
<div class="gf-form-select-wrapper"> |
||||
<select |
||||
class="gf-form-input gf-size-auto" |
||||
ng-model="ctrl.target.insightsAnalytics.resultFormat" |
||||
ng-options="f.value as f.text for f in ctrl.resultFormats" |
||||
ng-change="ctrl.refresh()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div ng-if="ctrl.target.queryType === 'Application Insights'"> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Metric</label> |
||||
<gf-form-dropdown |
||||
model="ctrl.target.appInsights.metricName" |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
get-options="ctrl.getAppInsightsMetricNames($query)" |
||||
on-change="ctrl.onAppInsightsMetricNameChange()" |
||||
css-class="min-width-20" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Aggregation</label> |
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent"> |
||||
<select |
||||
class="gf-form-input" |
||||
ng-model="ctrl.target.appInsights.aggregation" |
||||
ng-options="f as f for f in ctrl.target.appInsights.aggOptions" |
||||
ng-change="ctrl.refresh()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Group By</label> |
||||
</div> |
||||
<div |
||||
ng-repeat="d in ctrl.target.appInsights.dimension track by $index" |
||||
class="gf-form" |
||||
ng-click="ctrl.removeGroupBy($index);" |
||||
onmouseover="this.style['text-decoration'] = 'line-through';" |
||||
onmouseout="this.style['text-decoration'] = '';" |
||||
> |
||||
<label class="gf-form-label" style="cursor: pointer">{{d}} <icon name="'times'"></icon></label> |
||||
</div> |
||||
<div> |
||||
<gf-form-dropdown |
||||
allow-custom="true" |
||||
lookup-text="true" |
||||
placeholder="Add" |
||||
model="ctrl.dummyDiminsionString" |
||||
get-options="ctrl.getAppInsightsGroupBySegments($query)" |
||||
on-change="ctrl.getAppInsightsGroupBySegments" |
||||
css-class="min-width-5" |
||||
> |
||||
</gf-form-dropdown> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword">Filter</label> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-17" |
||||
ng-model="ctrl.target.appInsights.dimensionFilter" |
||||
spellcheck="false" |
||||
placeholder="your/groupby eq 'a_value'" |
||||
ng-blur="ctrl.refresh()" |
||||
/> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Time Grain</label> |
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent"> |
||||
<select |
||||
class="gf-form-input" |
||||
ng-model="ctrl.target.appInsights.timeGrainType" |
||||
ng-options="f as f for f in ['auto', 'none', 'specific']" |
||||
ng-change="ctrl.updateTimeGrainType()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div |
||||
class="gf-form" |
||||
ng-hide="ctrl.target.appInsights.timeGrainType === 'auto' || ctrl.target.appInsights.timeGrainType === 'none'" |
||||
> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-3" |
||||
ng-model="ctrl.target.appInsights.timeGrainCount" |
||||
spellcheck="false" |
||||
placeholder="" |
||||
ng-blur="ctrl.updateAppInsightsTimeGrain()" |
||||
/> |
||||
</div> |
||||
<div |
||||
class="gf-form" |
||||
ng-hide="ctrl.target.appInsights.timeGrainType === 'auto' || ctrl.target.appInsights.timeGrainType === 'none'" |
||||
> |
||||
<div class="gf-form-select-wrapper gf-form-select-wrapper--caret-indent timegrainunit-dropdown-wrapper"> |
||||
<select |
||||
class="gf-form-input" |
||||
ng-model="ctrl.target.appInsights.timeGrainUnit" |
||||
ng-options="f as f for f in ['minute', 'hour', 'day', 'month', 'year']" |
||||
ng-change="ctrl.updateAppInsightsTimeGrain()" |
||||
></select> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form" ng-hide="ctrl.target.appInsights.timeGrainType !== 'auto'"> |
||||
<label class="gf-form-label">Auto Interval (see query options)</label> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
<div class="gf-form-inline"> |
||||
<div class="gf-form"> |
||||
<label class="gf-form-label query-keyword width-9">Legend Format</label> |
||||
<input |
||||
type="text" |
||||
class="gf-form-input width-30" |
||||
ng-model="ctrl.target.appInsights.alias" |
||||
spellcheck="false" |
||||
placeholder="alias patterns (see help for more info)" |
||||
ng-blur="ctrl.refresh()" |
||||
/> |
||||
</div> |
||||
<div class="gf-form gf-form--grow"> |
||||
<div class="gf-form-label gf-form-label--grow"></div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
||||
<div class="gf-form" ng-show="ctrl.lastQueryError"> |
||||
<pre class="gf-form-pre alert alert-error">{{ctrl.lastQueryError}}</pre> |
||||
</div> |
||||
|
||||
<div |
||||
class="gf-form" |
||||
ng-if="ctrl.target.queryType === 'Insights Analytics'" |
||||
> |
||||
<p class="gf-form-pre alert alert-info"> |
||||
Application Insights and Insights Analytics will be deprecated and merged with Metrics and Logs in an upcoming |
||||
release. See |
||||
<a |
||||
class="text-link" |
||||
href="https://grafana.com/docs/grafana/latest/datasources/azuremonitor/#deprecating-application-insights-and-insights-analytics" |
||||
>the documentation</a |
||||
> |
||||
for more details. |
||||
</p> |
||||
</div> |
||||
</query-editor-row> |
||||
|
||||
<!-- Partial migration to React --> |
||||
<div ng-if="ctrl.reactQueryEditors.includes(ctrl.target.queryType)"> |
||||
<azure-monitor-query-editor query="ctrl.target" datasource="ctrl.datasource" on-change="ctrl.handleNewQuery"> |
||||
</azure-monitor-query-editor> |
||||
</div> |
||||
@ -1,138 +0,0 @@ |
||||
jest.mock('./css/query_editor.css', () => { |
||||
return {}; |
||||
}); |
||||
|
||||
import { AzureMonitorQueryCtrl } from './query_ctrl'; |
||||
// @ts-ignore
|
||||
import Q from 'q'; |
||||
import { TemplateSrv } from 'app/features/templating/template_srv'; |
||||
import { auto } from 'angular'; |
||||
|
||||
describe('AzureMonitorQueryCtrl', () => { |
||||
let queryCtrl: any; |
||||
|
||||
beforeEach(() => { |
||||
AzureMonitorQueryCtrl.prototype.panelCtrl = { |
||||
events: { on: () => {} }, |
||||
panel: { scopedVars: [], targets: [] }, |
||||
}; |
||||
AzureMonitorQueryCtrl.prototype.target = {} as any; |
||||
AzureMonitorQueryCtrl.prototype.datasource = { |
||||
appInsightsDatasource: { isConfigured: () => false }, |
||||
azureMonitorDatasource: { isConfigured: () => false }, |
||||
}; |
||||
|
||||
queryCtrl = new AzureMonitorQueryCtrl({}, {} as auto.IInjectorService, new TemplateSrv()); |
||||
}); |
||||
|
||||
describe('init query_ctrl variables', () => { |
||||
it('should set default query type to Azure Monitor', () => { |
||||
expect(queryCtrl.target.queryType).toBe('Azure Monitor'); |
||||
}); |
||||
|
||||
it('should set default App Insights editor to be builder', () => { |
||||
expect(!!(queryCtrl.target.appInsights as any).rawQuery).toBe(false); |
||||
}); |
||||
|
||||
it('should set query parts to select', () => { |
||||
// expect(queryCtrl.target.azureMonitor.resourceGroup).toBe('select');
|
||||
// expect(queryCtrl.target.azureMonitor.metricDefinition).toBe('select');
|
||||
// expect(queryCtrl.target.azureMonitor.resourceName).toBe('select');
|
||||
// expect(queryCtrl.target.azureMonitor.metricNamespace).toBe('select');
|
||||
// expect(queryCtrl.target.azureMonitor.metricName).toBe('select');
|
||||
expect(queryCtrl.target.appInsights.dimension).toMatchObject([]); |
||||
}); |
||||
}); |
||||
|
||||
describe('and query type is Application Insights', () => { |
||||
describe('and target is in old format', () => { |
||||
it('data is migrated', () => { |
||||
queryCtrl.target.appInsights.xaxis = 'sample-x'; |
||||
queryCtrl.target.appInsights.yaxis = 'sample-y'; |
||||
queryCtrl.target.appInsights.spliton = 'sample-split'; |
||||
queryCtrl.target.appInsights.groupBy = 'sample-group'; |
||||
queryCtrl.target.appInsights.groupByOptions = ['sample-group-1', 'sample-group-2']; |
||||
queryCtrl.target.appInsights.filter = 'sample-filter'; |
||||
queryCtrl.target.appInsights.metricName = 'sample-metric'; |
||||
|
||||
queryCtrl.migrateApplicationInsightsKeys(); |
||||
|
||||
expect(queryCtrl.target.appInsights.xaxis).toBeUndefined(); |
||||
expect(queryCtrl.target.appInsights.yaxis).toBeUndefined(); |
||||
expect(queryCtrl.target.appInsights.spliton).toBeUndefined(); |
||||
expect(queryCtrl.target.appInsights.groupBy).toBeUndefined(); |
||||
expect(queryCtrl.target.appInsights.groupByOptions).toBeUndefined(); |
||||
expect(queryCtrl.target.appInsights.filter).toBeUndefined(); |
||||
|
||||
expect(queryCtrl.target.appInsights.timeColumn).toBe('sample-x'); |
||||
expect(queryCtrl.target.appInsights.valueColumn).toBe('sample-y'); |
||||
expect(queryCtrl.target.appInsights.segmentColumn).toBe('sample-split'); |
||||
expect(queryCtrl.target.appInsights.dimension).toBe('sample-group'); |
||||
expect(queryCtrl.target.appInsights.dimensions).toEqual(['sample-group-1', 'sample-group-2']); |
||||
expect(queryCtrl.target.appInsights.dimensionFilter).toBe('sample-filter'); |
||||
expect(queryCtrl.target.appInsights.metricName).toBe('sample-metric'); |
||||
}); |
||||
}); |
||||
|
||||
describe('when getOptions for the Metric Names dropdown is called', () => { |
||||
const response = [ |
||||
{ text: 'metric1', value: 'metric1' }, |
||||
{ text: 'metric2', value: 'metric2' }, |
||||
]; |
||||
|
||||
beforeEach(() => { |
||||
queryCtrl.datasource.appInsightsDatasource.isConfigured = () => true; |
||||
queryCtrl.datasource.getAppInsightsMetricNames = () => { |
||||
return Promise.resolve(response); |
||||
}; |
||||
}); |
||||
|
||||
it('should return a list of Metric Names', () => { |
||||
return queryCtrl.getAppInsightsMetricNames().then((result: any) => { |
||||
expect(result[0].text).toBe('metric1'); |
||||
expect(result[1].text).toBe('metric2'); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
describe('when getOptions for the GroupBy segments dropdown is called', () => { |
||||
beforeEach(() => { |
||||
queryCtrl.target.appInsights.dimensions = ['opt1', 'opt2']; |
||||
}); |
||||
|
||||
it('should return a list of GroupBy segments', () => { |
||||
const result = queryCtrl.getAppInsightsGroupBySegments(''); |
||||
expect(result[0].text).toBe('opt1'); |
||||
expect(result[0].value).toBe('opt1'); |
||||
expect(result[1].text).toBe('opt2'); |
||||
expect(result[1].value).toBe('opt2'); |
||||
}); |
||||
}); |
||||
|
||||
describe('when onAppInsightsMetricNameChange is triggered for the Metric Names dropdown', () => { |
||||
const response = { |
||||
primaryAggType: 'avg', |
||||
supportedAggTypes: ['avg', 'sum'], |
||||
supportedGroupBy: ['client/os', 'client/city'], |
||||
}; |
||||
|
||||
beforeEach(() => { |
||||
queryCtrl.target.appInsights.metricName = 'requests/failed'; |
||||
queryCtrl.datasource.getAppInsightsMetricMetadata = (metricName: string) => { |
||||
expect(metricName).toBe('requests/failed'); |
||||
return Promise.resolve(response); |
||||
}; |
||||
}); |
||||
|
||||
it('should set the options and default selected value for the Aggregations dropdown', () => { |
||||
return queryCtrl.onAppInsightsMetricNameChange().then(() => { |
||||
expect(queryCtrl.target.appInsights.aggregation).toBe('avg'); |
||||
expect(queryCtrl.target.appInsights.aggOptions).toContain('avg'); |
||||
expect(queryCtrl.target.appInsights.aggOptions).toContain('sum'); |
||||
expect(queryCtrl.target.appInsights.dimensions).toContain('client/os'); |
||||
expect(queryCtrl.target.appInsights.dimensions).toContain('client/city'); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -1,569 +0,0 @@ |
||||
import { defaultsDeep, find, map, isString } from 'lodash'; |
||||
import { QueryCtrl } from 'app/plugins/sdk'; |
||||
import TimegrainConverter from './time_grain_converter'; |
||||
import './editor/editor_component'; |
||||
|
||||
import { TemplateSrv } from '@grafana/runtime'; |
||||
import { auto } from 'angular'; |
||||
import { DataFrame, PanelEvents } from '@grafana/data'; |
||||
import { AzureQueryType, AzureMetricQuery, AzureMonitorQuery } from './types'; |
||||
import { convertTimeGrainsToMs } from './utils/common'; |
||||
import Datasource from './datasource'; |
||||
|
||||
export interface ResultFormat { |
||||
text: string; |
||||
value: string; |
||||
} |
||||
|
||||
export class AzureMonitorQueryCtrl extends QueryCtrl { |
||||
static templateUrl = 'partials/query.editor.html'; |
||||
|
||||
defaultDropdownValue = 'select'; |
||||
|
||||
dummyDiminsionString = '+'; |
||||
|
||||
queryQueryTypeOptions = [ |
||||
{ id: AzureQueryType.AzureMonitor, label: 'Metrics' }, |
||||
{ id: AzureQueryType.LogAnalytics, label: 'Logs' }, |
||||
{ id: AzureQueryType.ApplicationInsights, label: 'Application Insights' }, |
||||
{ id: AzureQueryType.InsightsAnalytics, label: 'Insights Analytics' }, |
||||
{ id: AzureQueryType.AzureResourceGraph, label: 'Azure Resource Graph' }, |
||||
]; |
||||
|
||||
// Query types that have been migrated to React
|
||||
reactQueryEditors = [ |
||||
AzureQueryType.AzureMonitor, |
||||
AzureQueryType.LogAnalytics, |
||||
AzureQueryType.ApplicationInsights, |
||||
AzureQueryType.InsightsAnalytics, |
||||
AzureQueryType.AzureResourceGraph, |
||||
]; |
||||
|
||||
// target: AzureMonitorQuery;
|
||||
|
||||
declare target: { |
||||
// should be: AzureMonitorQuery
|
||||
refId: string; |
||||
queryType: AzureQueryType; |
||||
subscription: string; |
||||
subscriptions: string[]; |
||||
azureMonitor: AzureMetricQuery; |
||||
azureLogAnalytics: { |
||||
query: string; |
||||
resultFormat: string; |
||||
workspace: string; |
||||
}; |
||||
azureResourceGraph: { |
||||
query: string; |
||||
resultFormat: string; |
||||
}; |
||||
appInsights: { |
||||
// metric style query when rawQuery == false
|
||||
metricName: string; |
||||
dimension: any; |
||||
dimensionFilter: string; |
||||
dimensions: string[]; |
||||
|
||||
aggOptions: string[]; |
||||
aggregation: string; |
||||
|
||||
timeGrainType: string; |
||||
timeGrainCount: string; |
||||
timeGrainUnit: string; |
||||
timeGrain: string; |
||||
timeGrains: Array<{ text: string; value: string }>; |
||||
allowedTimeGrainsMs: number[]; |
||||
}; |
||||
insightsAnalytics: { |
||||
query: any; |
||||
resultFormat: string; |
||||
}; |
||||
}; |
||||
|
||||
defaults = { |
||||
queryType: 'Azure Monitor', |
||||
azureMonitor: { |
||||
resourceGroup: undefined, |
||||
metricDefinition: undefined, |
||||
resourceName: undefined, |
||||
metricNamespace: undefined, |
||||
metricName: undefined, |
||||
dimensionFilter: '*', |
||||
timeGrain: 'auto', |
||||
top: '10', |
||||
aggOptions: [] as string[], |
||||
timeGrains: [] as string[], |
||||
}, |
||||
azureLogAnalytics: { |
||||
query: [ |
||||
'//change this example to create your own time series query', |
||||
'<table name> ' + |
||||
'//the table to query (e.g. Usage, Heartbeat, Perf)', |
||||
'| where $__timeFilter(TimeGenerated) ' + |
||||
'//this is a macro used to show the full chart’s time range, choose the datetime column here', |
||||
'| summarize count() by <group by column>, bin(TimeGenerated, $__interval) ' + |
||||
'//change “group by column” to a column in your table, such as “Computer”. ' + |
||||
'The $__interval macro is used to auto-select the time grain. Can also use 1h, 5m etc.', |
||||
'| order by TimeGenerated asc', |
||||
].join('\n'), |
||||
resultFormat: 'time_series', |
||||
workspace: |
||||
this.datasource && this.datasource.azureLogAnalyticsDatasource |
||||
? this.datasource.azureLogAnalyticsDatasource.defaultOrFirstWorkspace |
||||
: '', |
||||
}, |
||||
azureResourceGraph: { |
||||
resultFormat: 'table', |
||||
}, |
||||
appInsights: { |
||||
metricName: this.defaultDropdownValue, |
||||
// dimension: [],
|
||||
timeGrain: 'auto', |
||||
}, |
||||
insightsAnalytics: { |
||||
query: '', |
||||
resultFormat: 'time_series', |
||||
}, |
||||
}; |
||||
|
||||
resultFormats: ResultFormat[]; |
||||
workspaces: any[] = []; |
||||
showHelp = false; |
||||
showLastQuery = false; |
||||
lastQuery = ''; |
||||
lastQueryError?: string; |
||||
subscriptions: Array<{ text: string; value: string }> = []; |
||||
|
||||
/** @ngInject */ |
||||
constructor($scope: any, $injector: auto.IInjectorService, private templateSrv: TemplateSrv) { |
||||
super($scope, $injector); |
||||
|
||||
defaultsDeep(this.target, this.defaults); |
||||
|
||||
this.migrateTimeGrains(); |
||||
|
||||
this.migrateToFromTimes(); |
||||
|
||||
this.migrateToDefaultNamespace(); |
||||
|
||||
this.migrateApplicationInsightsKeys(); |
||||
|
||||
this.migrateApplicationInsightsDimensions(); |
||||
|
||||
migrateMetricsDimensionFilters(this.target.azureMonitor); |
||||
|
||||
this.panelCtrl.events.on(PanelEvents.dataReceived, this.onDataReceived.bind(this), $scope); |
||||
this.panelCtrl.events.on(PanelEvents.dataError, this.onDataError.bind(this), $scope); |
||||
this.resultFormats = [ |
||||
{ text: 'Time series', value: 'time_series' }, |
||||
{ text: 'Table', value: 'table' }, |
||||
]; |
||||
this.getSubscriptions(); |
||||
if (this.target.queryType === 'Azure Log Analytics') { |
||||
this.getWorkspaces(); |
||||
} |
||||
} |
||||
|
||||
onDataReceived(dataList: DataFrame[]) { |
||||
this.lastQueryError = undefined; |
||||
this.lastQuery = ''; |
||||
|
||||
const anySeriesFromQuery: any = find(dataList, { refId: this.target.refId }); |
||||
if (anySeriesFromQuery && anySeriesFromQuery.meta) { |
||||
this.lastQuery = anySeriesFromQuery.meta.query; |
||||
} |
||||
} |
||||
|
||||
onDataError(err: any) { |
||||
this.handleQueryCtrlError(err); |
||||
} |
||||
|
||||
handleQueryCtrlError(err: any) { |
||||
if (err.query && err.query.refId && err.query.refId !== this.target.refId) { |
||||
return; |
||||
} |
||||
|
||||
if (err.error && err.error.data && err.error.data.error && err.error.data.error.innererror) { |
||||
if (err.error.data.error.innererror.innererror) { |
||||
this.lastQueryError = err.error.data.error.innererror.innererror.message; |
||||
} else { |
||||
this.lastQueryError = err.error.data.error.innererror.message; |
||||
} |
||||
} else if (err.error && err.error.data && err.error.data.error) { |
||||
this.lastQueryError = err.error.data.error.message; |
||||
} else if (err.error && err.error.data) { |
||||
this.lastQueryError = err.error.data.message; |
||||
} else if (err.data && err.data.error) { |
||||
this.lastQueryError = err.data.error.message; |
||||
} else if (err.data && err.data.message) { |
||||
this.lastQueryError = err.data.message; |
||||
} else { |
||||
this.lastQueryError = err; |
||||
} |
||||
} |
||||
|
||||
migrateTimeGrains() { |
||||
if (this.target.azureMonitor.timeGrainUnit) { |
||||
if (this.target.azureMonitor.timeGrain !== 'auto') { |
||||
this.target.azureMonitor.timeGrain = TimegrainConverter.createISO8601Duration( |
||||
this.target.azureMonitor.timeGrain ?? 'auto', |
||||
this.target.azureMonitor.timeGrainUnit |
||||
); |
||||
} |
||||
|
||||
delete this.target.azureMonitor.timeGrainUnit; |
||||
} |
||||
|
||||
if (this.target.appInsights.timeGrainUnit) { |
||||
if (this.target.appInsights.timeGrain !== 'auto') { |
||||
if (this.target.appInsights.timeGrainCount) { |
||||
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration( |
||||
this.target.appInsights.timeGrainCount, |
||||
this.target.appInsights.timeGrainUnit |
||||
); |
||||
} else { |
||||
this.target.appInsights.timeGrainCount = this.target.appInsights.timeGrain; |
||||
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration( |
||||
this.target.appInsights.timeGrain, |
||||
this.target.appInsights.timeGrainUnit |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
const oldAzureTimeGrains = (this.target.azureMonitor as any).timeGrains; |
||||
if ( |
||||
oldAzureTimeGrains && |
||||
oldAzureTimeGrains.length > 0 && |
||||
(!this.target.azureMonitor.allowedTimeGrainsMs || this.target.azureMonitor.allowedTimeGrainsMs.length === 0) |
||||
) { |
||||
this.target.azureMonitor.allowedTimeGrainsMs = convertTimeGrainsToMs(oldAzureTimeGrains); |
||||
} |
||||
|
||||
if ( |
||||
this.target.appInsights.timeGrains && |
||||
this.target.appInsights.timeGrains.length > 0 && |
||||
(!this.target.appInsights.allowedTimeGrainsMs || this.target.appInsights.allowedTimeGrainsMs.length === 0) |
||||
) { |
||||
this.target.appInsights.allowedTimeGrainsMs = convertTimeGrainsToMs(this.target.appInsights.timeGrains); |
||||
} |
||||
} |
||||
|
||||
migrateToFromTimes() { |
||||
this.target.azureLogAnalytics.query = this.target.azureLogAnalytics.query.replace(/\$__from\s/gi, '$__timeFrom() '); |
||||
this.target.azureLogAnalytics.query = this.target.azureLogAnalytics.query.replace(/\$__to\s/gi, '$__timeTo() '); |
||||
} |
||||
|
||||
async migrateToDefaultNamespace() { |
||||
if ( |
||||
this.target.azureMonitor.metricNamespace && |
||||
this.target.azureMonitor.metricNamespace !== this.defaultDropdownValue && |
||||
this.target.azureMonitor.metricDefinition |
||||
) { |
||||
return; |
||||
} |
||||
|
||||
this.target.azureMonitor.metricNamespace = this.target.azureMonitor.metricDefinition; |
||||
} |
||||
|
||||
migrateApplicationInsightsKeys(): void { |
||||
const appInsights = this.target.appInsights as any; |
||||
|
||||
// Migrate old app insights data keys to match other datasources
|
||||
const mappings = { |
||||
xaxis: 'timeColumn', |
||||
yaxis: 'valueColumn', |
||||
spliton: 'segmentColumn', |
||||
groupBy: 'dimension', |
||||
groupByOptions: 'dimensions', |
||||
filter: 'dimensionFilter', |
||||
} as { [old: string]: string }; |
||||
|
||||
for (const old in mappings) { |
||||
if (appInsights[old]) { |
||||
appInsights[mappings[old]] = appInsights[old]; |
||||
delete appInsights[old]; |
||||
} |
||||
} |
||||
} |
||||
|
||||
migrateApplicationInsightsDimensions() { |
||||
const { appInsights } = this.target; |
||||
|
||||
if (!appInsights.dimension) { |
||||
appInsights.dimension = []; |
||||
} |
||||
|
||||
if (isString(appInsights.dimension)) { |
||||
appInsights.dimension = [appInsights.dimension as string]; |
||||
} |
||||
} |
||||
|
||||
replace = (variable: string) => { |
||||
return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars); |
||||
}; |
||||
|
||||
onQueryTypeChange() { |
||||
if (this.target.queryType === 'Azure Log Analytics') { |
||||
return this.getWorkspaces(); |
||||
} |
||||
} |
||||
|
||||
getSubscriptions() { |
||||
if (!this.datasource.azureMonitorDatasource.isConfigured()) { |
||||
return; |
||||
} |
||||
|
||||
// assert the type
|
||||
if (!(this.datasource instanceof Datasource)) { |
||||
return; |
||||
} |
||||
|
||||
return this.datasource.azureMonitorDatasource.getSubscriptions().then((subscriptions) => { |
||||
// We changed the format in the datasource for the new react stuff, so here we change it back
|
||||
const subs = subscriptions.map((v) => ({ |
||||
text: `${v.text} - ${v.value}`, |
||||
value: v.value, |
||||
})); |
||||
|
||||
this.subscriptions = subs; |
||||
if (!this.target.subscription && this.target.queryType === 'Azure Monitor') { |
||||
this.target.subscription = this.datasource.azureMonitorDatasource.subscriptionId; |
||||
} else if (!this.target.subscription && this.target.queryType === 'Azure Log Analytics') { |
||||
this.target.subscription = this.datasource.azureLogAnalyticsDatasource.subscriptionId; |
||||
} |
||||
|
||||
if (!this.target.subscription && this.subscriptions.length > 0) { |
||||
this.target.subscription = this.subscriptions[0].value; |
||||
} |
||||
|
||||
if (!this.target.subscriptions) { |
||||
this.target.subscriptions = subscriptions.map((sub) => sub.value); |
||||
} |
||||
|
||||
return this.subscriptions; |
||||
}); |
||||
} |
||||
|
||||
onSubscriptionChange() { |
||||
if (this.target.queryType === 'Azure Log Analytics') { |
||||
return this.getWorkspaces(); |
||||
} |
||||
} |
||||
|
||||
generateAutoUnits(timeGrain: string, timeGrains: Array<{ value: string }>) { |
||||
if (timeGrain === 'auto') { |
||||
return TimegrainConverter.findClosestTimeGrain( |
||||
'1m', |
||||
map(timeGrains, (o) => TimegrainConverter.createKbnUnitFromISO8601Duration(o.value)) || [ |
||||
'1m', |
||||
'5m', |
||||
'15m', |
||||
'30m', |
||||
'1h', |
||||
'6h', |
||||
'12h', |
||||
'1d', |
||||
] |
||||
); |
||||
} |
||||
|
||||
return ''; |
||||
} |
||||
|
||||
getAzureMonitorAutoInterval() { |
||||
return this.generateAutoUnits( |
||||
this.target.azureMonitor.timeGrain ?? 'auto', |
||||
(this.target.azureMonitor as any).timeGrains |
||||
); |
||||
} |
||||
|
||||
getApplicationInsightAutoInterval() { |
||||
return this.generateAutoUnits(this.target.appInsights.timeGrain, this.target.appInsights.timeGrains); |
||||
} |
||||
|
||||
azureMonitorAddDimensionFilter() { |
||||
this.target.azureMonitor = this.target.azureMonitor ?? {}; |
||||
this.target.azureMonitor.dimensionFilters = this.target.azureMonitor.dimensionFilters ?? []; |
||||
|
||||
this.target.azureMonitor.dimensionFilters.push({ |
||||
dimension: '', |
||||
operator: 'eq', |
||||
filter: '', |
||||
}); |
||||
} |
||||
|
||||
azureMonitorRemoveDimensionFilter(index: number) { |
||||
this.target.azureMonitor = this.target.azureMonitor ?? {}; |
||||
this.target.azureMonitor.dimensionFilters = this.target.azureMonitor.dimensionFilters ?? []; |
||||
|
||||
this.target.azureMonitor.dimensionFilters.splice(index, 1); |
||||
this.refresh(); |
||||
} |
||||
|
||||
/* Azure Log Analytics */ |
||||
|
||||
getWorkspaces = () => { |
||||
return this.datasource.azureLogAnalyticsDatasource |
||||
.getWorkspaces(this.target.subscription) |
||||
.then((list: any[]) => { |
||||
this.workspaces = list; |
||||
|
||||
if (list.length > 0 && !this.target.azureLogAnalytics.workspace) { |
||||
if (this.datasource.azureLogAnalyticsDatasource.defaultOrFirstWorkspace) { |
||||
this.target.azureLogAnalytics.workspace = this.datasource.azureLogAnalyticsDatasource.defaultOrFirstWorkspace; |
||||
} |
||||
|
||||
if (!this.target.azureLogAnalytics.workspace) { |
||||
this.target.azureLogAnalytics.workspace = list[0].value; |
||||
} |
||||
} |
||||
|
||||
return this.workspaces; |
||||
}) |
||||
.catch(this.handleQueryCtrlError.bind(this)); |
||||
}; |
||||
|
||||
getAzureLogAnalyticsSchema = () => { |
||||
return this.getWorkspaces() |
||||
.then(() => { |
||||
return this.datasource.azureLogAnalyticsDatasource.getSchema(this.target.azureLogAnalytics.workspace); |
||||
}) |
||||
.catch(this.handleQueryCtrlError.bind(this)); |
||||
}; |
||||
|
||||
onLogAnalyticsQueryChange = (nextQuery: string) => { |
||||
this.target.azureLogAnalytics.query = nextQuery; |
||||
}; |
||||
|
||||
onLogAnalyticsQueryExecute = () => { |
||||
this.panelCtrl.refresh(); |
||||
}; |
||||
|
||||
get templateVariables() { |
||||
return this.templateSrv.getVariables().map((t) => '$' + t.name); |
||||
} |
||||
|
||||
getAppInsightsMetricNames() { |
||||
if (!this.datasource.appInsightsDatasource.isConfigured()) { |
||||
return; |
||||
} |
||||
|
||||
return this.datasource.getAppInsightsMetricNames().catch(this.handleQueryCtrlError.bind(this)); |
||||
} |
||||
|
||||
getAppInsightsColumns() { |
||||
return this.datasource.getAppInsightsColumns(this.target.refId); |
||||
} |
||||
|
||||
onAppInsightsColumnChange() { |
||||
return this.refresh(); |
||||
} |
||||
|
||||
onAppInsightsMetricNameChange() { |
||||
if (!this.target.appInsights.metricName || this.target.appInsights.metricName === this.defaultDropdownValue) { |
||||
return; |
||||
} |
||||
|
||||
return this.datasource |
||||
.getAppInsightsMetricMetadata(this.replace(this.target.appInsights.metricName)) |
||||
.then((aggData: { supportedAggTypes: string[]; supportedGroupBy: string[]; primaryAggType: string }) => { |
||||
this.target.appInsights.aggOptions = aggData.supportedAggTypes; |
||||
this.target.appInsights.dimensions = aggData.supportedGroupBy; |
||||
this.target.appInsights.aggregation = aggData.primaryAggType; |
||||
return this.refresh(); |
||||
}) |
||||
.catch(this.handleQueryCtrlError.bind(this)); |
||||
} |
||||
|
||||
onInsightsAnalyticsQueryChange = (nextQuery: string) => { |
||||
this.target.insightsAnalytics.query = nextQuery; |
||||
}; |
||||
|
||||
onQueryExecute = () => { |
||||
return this.refresh(); |
||||
}; |
||||
|
||||
getAppInsightsQuerySchema = () => { |
||||
return this.datasource.appInsightsDatasource.getQuerySchema().catch(this.handleQueryCtrlError.bind(this)); |
||||
}; |
||||
|
||||
removeGroupBy = (index: number) => { |
||||
const { appInsights } = this.target; |
||||
appInsights.dimension.splice(index, 1); |
||||
this.refresh(); |
||||
}; |
||||
|
||||
getAppInsightsGroupBySegments(query: any) { |
||||
const { appInsights } = this.target; |
||||
|
||||
// HACK alert... there must be a better way!
|
||||
if (this.dummyDiminsionString && this.dummyDiminsionString.length && '+' !== this.dummyDiminsionString) { |
||||
if (!appInsights.dimension) { |
||||
appInsights.dimension = []; |
||||
} |
||||
appInsights.dimension.push(this.dummyDiminsionString); |
||||
this.dummyDiminsionString = '+'; |
||||
this.refresh(); |
||||
} |
||||
|
||||
// Return the list of dimensions stored on the query object from the last request :(
|
||||
return map(appInsights.dimensions, (option: string) => { |
||||
return { text: option, value: option }; |
||||
}); |
||||
} |
||||
|
||||
resetAppInsightsGroupBy() { |
||||
this.target.appInsights.dimension = 'none'; |
||||
this.refresh(); |
||||
} |
||||
|
||||
updateTimeGrainType() { |
||||
if (this.target.appInsights.timeGrainType === 'specific') { |
||||
this.target.appInsights.timeGrainCount = '1'; |
||||
this.target.appInsights.timeGrainUnit = 'minute'; |
||||
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration( |
||||
this.target.appInsights.timeGrainCount, |
||||
this.target.appInsights.timeGrainUnit |
||||
); |
||||
} else { |
||||
this.target.appInsights.timeGrainCount = ''; |
||||
this.target.appInsights.timeGrainUnit = ''; |
||||
} |
||||
} |
||||
|
||||
updateAppInsightsTimeGrain() { |
||||
if (this.target.appInsights.timeGrainUnit && this.target.appInsights.timeGrainCount) { |
||||
this.target.appInsights.timeGrain = TimegrainConverter.createISO8601Duration( |
||||
this.target.appInsights.timeGrainCount, |
||||
this.target.appInsights.timeGrainUnit |
||||
); |
||||
} |
||||
this.refresh(); |
||||
} |
||||
|
||||
/** |
||||
* Receives a full new query object from React and updates it into the Angular controller |
||||
*/ |
||||
handleNewQuery = (newQuery: AzureMonitorQuery) => { |
||||
Object.assign(this.target, newQuery); |
||||
this.refresh(); |
||||
}; |
||||
} |
||||
|
||||
// Modifies the actual query object
|
||||
export function migrateMetricsDimensionFilters(item: AzureMetricQuery) { |
||||
if (!item.dimensionFilters) { |
||||
item.dimensionFilters = []; |
||||
} |
||||
const oldDimension = (item as any).dimension; |
||||
if (oldDimension && oldDimension !== 'None') { |
||||
item.dimensionFilters.push({ |
||||
dimension: oldDimension, |
||||
operator: 'eq', |
||||
filter: (item as any).dimensionFilter, |
||||
}); |
||||
delete (item as any).dimension; |
||||
delete (item as any).dimensionFilter; |
||||
} |
||||
} |
||||
Loading…
Reference in new issue