mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AzureMonitor: Remove unused editor component (#39874)
This commit is contained in:
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,720 +0,0 @@
|
||||
/* eslint-disable max-len */
|
||||
export const operatorTokens = [
|
||||
{ text: '!between', hint: 'Matches the input that is outside the inclusive range.' },
|
||||
{ text: 'as', hint: "Binds a name to the operator's input tabular expression." },
|
||||
{ text: 'between', hint: 'Matches the input that is inside the inclusive range.' },
|
||||
{
|
||||
text: 'consume',
|
||||
hint:
|
||||
'The `consume` operator consumes the tabular data stream handed to it. It is\r\nmostly used for triggering the query side-effect without actually returning\r\nthe results back to the caller.',
|
||||
},
|
||||
{ text: 'count', hint: 'Returns the number of records in the input record set.' },
|
||||
{ text: 'datatable', hint: 'Returns a table whose schema and values are defined in the query itself.' },
|
||||
{
|
||||
text: 'distinct',
|
||||
hint: 'Produces a table with the distinct combination of the provided columns of the input table.',
|
||||
},
|
||||
{ text: 'evaluate', hint: 'Invokes a service-side query extension (plugin).' },
|
||||
{ text: 'extend', hint: 'Create calculated columns and append them to the result set.' },
|
||||
{
|
||||
text: 'externaldata',
|
||||
hint:
|
||||
'Returns a table whose schema is defined in the query itself, and whose data is read from an external raw file.',
|
||||
},
|
||||
{
|
||||
text: 'facet',
|
||||
hint:
|
||||
'Returns a set of tables, one for each specified column.\r\nEach table specifies the list of values taken by its column.\r\nAn additional table can be created by using the `with` clause.',
|
||||
},
|
||||
{ text: 'find', hint: 'Finds rows that match a predicate across a set of tables.' },
|
||||
{ text: 'fork', hint: 'Runs multiple consumer operators in parallel.' },
|
||||
{ text: 'getschema', hint: 'Produce a table that represents a tabular schema of the input.' },
|
||||
{ text: 'in', hint: 'Filters a recordset based on the provided set of values.' },
|
||||
{ text: 'invoke', hint: 'Invokes lambda that receives the source of `invoke` as tabular parameter argument.' },
|
||||
{
|
||||
text: 'join',
|
||||
hint:
|
||||
'Merge the rows of two tables to form a new table by matching values of the specified column(s) from each table.',
|
||||
},
|
||||
{ text: 'limit', hint: 'Return up to the specified number of rows.' },
|
||||
{ text: 'make-series', hint: 'Create series of specified aggregated values along specified axis.' },
|
||||
{ text: 'mvexpand', hint: 'Expands multi-value array or property bag.' },
|
||||
{ text: 'order', hint: 'Sort the rows of the input table into order by one or more columns.' },
|
||||
{ text: 'parse', hint: 'Evaluates a string expression and parses its value into one or more calculated columns.' },
|
||||
{
|
||||
text: 'print',
|
||||
hint:
|
||||
'Evaluates one or more scalar expressions and inserts the results (as a single-row table with as many columns as there are expressions) into the output.',
|
||||
},
|
||||
{ text: 'project', hint: 'Select the columns to include, rename or drop, and insert new computed columns.' },
|
||||
{ text: 'project-away', hint: 'Select what columns to exclude from the input.' },
|
||||
{ text: 'project-rename', hint: 'Renames columns in the result output.' },
|
||||
{ text: 'range', hint: 'Generates a single-column table of values.' },
|
||||
{ text: 'reduce', hint: 'Groups a set of strings together based on values similarity.' },
|
||||
{ text: 'render', hint: 'Instructs the user agent to render the results of the query in a particular way.' },
|
||||
{ text: 'sample', hint: 'Returns up to the specified number of random rows from the input table.' },
|
||||
{
|
||||
text: 'sample-distinct',
|
||||
hint:
|
||||
'Returns a single column that contains up to the specified number of distinct values of the requested column.',
|
||||
},
|
||||
{ text: 'search', hint: 'The search operator provides a multi-table/multi-column search experience.' },
|
||||
{ text: 'serialize', hint: 'Marks that order of the input row set is safe for window functions usage.' },
|
||||
{ text: 'sort', hint: 'Sort the rows of the input table into order by one or more columns.' },
|
||||
{ text: 'summarize', hint: 'Produces a table that aggregates the content of the input table.' },
|
||||
{ text: 'take', hint: 'Return up to the specified number of rows.' },
|
||||
{ text: 'top', hint: 'Returns the first *N* records sorted by the specified columns.' },
|
||||
{
|
||||
text: 'top-hitters',
|
||||
hint: 'Returns an approximation of the first *N* results (assuming skewed distribution of the input).',
|
||||
},
|
||||
{
|
||||
text: 'top-nested',
|
||||
hint: 'Produces hierarchical top results, where each level is a drill-down based on previous level values.',
|
||||
},
|
||||
{ text: 'union', hint: 'Takes two or more tables and returns the rows of all of them.' },
|
||||
{ text: 'where', hint: 'Filters a table to the subset of rows that satisfy a predicate.' },
|
||||
];
|
||||
|
||||
export const functionTokens = [
|
||||
{ text: 'abs', hint: 'Calculates the absolute value of the input.' },
|
||||
{
|
||||
text: 'acos',
|
||||
hint:
|
||||
'Returns the angle whose cosine is the specified number (the inverse operation of [`cos()`](cosfunction.md)) .',
|
||||
},
|
||||
{ text: 'ago', hint: 'Subtracts the given timespan from the current UTC clock time.' },
|
||||
{ text: 'any', hint: 'Returns random non-empty value from the specified expression values.' },
|
||||
{
|
||||
text: 'arg_max',
|
||||
hint:
|
||||
'Finds a row in the group that maximizes *ExprToMaximize*, and returns the value of *ExprToReturn* (or `*` to return the entire row).',
|
||||
},
|
||||
{
|
||||
text: 'arg_min',
|
||||
hint:
|
||||
'Finds a row in the group that minimizes *ExprToMinimize*, and returns the value of *ExprToReturn* (or `*` to return the entire row).',
|
||||
},
|
||||
{
|
||||
text: 'argmax',
|
||||
hint:
|
||||
'Finds a row in the group that maximizes *ExprToMaximize*, and returns the value of *ExprToReturn* (or `*` to return the entire row).',
|
||||
},
|
||||
{
|
||||
text: 'argmin',
|
||||
hint:
|
||||
'Finds a row in the group that minimizes *ExprToMinimize*, and returns the value of *ExprToReturn* (or `*` to return the entire row).',
|
||||
},
|
||||
{ text: 'array_concat', hint: 'Concatenates a number of dynamic arrays to a single array.' },
|
||||
{ text: 'array_length', hint: 'Calculates the number of elements in a dynamic array.' },
|
||||
{ text: 'array_slice', hint: 'Extracts a slice of a dynamic array.' },
|
||||
{
|
||||
text: 'array_split',
|
||||
hint:
|
||||
'Splits an array to multiple arrays according to the split indices and packs the generated array in a dynamic array.',
|
||||
},
|
||||
{
|
||||
text: 'asin',
|
||||
hint: 'Returns the angle whose sine is the specified number (the inverse operation of [`sin()`](sinfunction.md)) .',
|
||||
},
|
||||
{
|
||||
text: 'assert',
|
||||
hint: 'Checks for a condition; if the condition is false, outputs error messages and fails the query.',
|
||||
},
|
||||
{
|
||||
text: 'atan',
|
||||
hint:
|
||||
'Returns the angle whose tangent is the specified number (the inverse operation of [`tan()`](tanfunction.md)) .',
|
||||
},
|
||||
{
|
||||
text: 'atan2',
|
||||
hint:
|
||||
'Calculates the angle, in radians, between the positive x-axis and the ray from the origin to the point (y, x).',
|
||||
},
|
||||
{ text: 'avg', hint: 'Calculates the average of *Expr* across the group.' },
|
||||
{
|
||||
text: 'avgif',
|
||||
hint:
|
||||
'Calculates the [average](avg-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`.',
|
||||
},
|
||||
{ text: 'bag_keys', hint: 'Enumerates all the root keys in a dynamic property-bag object.' },
|
||||
{ text: 'base64_decodestring', hint: 'Decodes a base64 string to a UTF-8 string' },
|
||||
{ text: 'base64_encodestring', hint: 'Encodes a string as base64 string' },
|
||||
{ text: 'beta_cdf', hint: 'Returns the standard cumulative beta distribution function.' },
|
||||
{ text: 'beta_inv', hint: 'Returns the inverse of the beta cumulative probability beta density function.' },
|
||||
{ text: 'beta_pdf', hint: 'Returns the probability density beta function.' },
|
||||
{ text: 'bin', hint: 'Rounds values down to an integer multiple of a given bin size.' },
|
||||
{
|
||||
text: 'bin_at',
|
||||
hint:
|
||||
"Rounds values down to a fixed-size 'bin', with control over the bin's starting point.\r\n(See also [`bin function`](./binfunction.md).)",
|
||||
},
|
||||
{
|
||||
text: 'bin_auto',
|
||||
hint:
|
||||
"Rounds values down to a fixed-size 'bin', with control over the bin size and starting point provided by a query property.",
|
||||
},
|
||||
{ text: 'binary_and', hint: 'Returns a result of the bitwise `and` operation between two values.' },
|
||||
{ text: 'binary_not', hint: 'Returns a bitwise negation of the input value.' },
|
||||
{ text: 'binary_or', hint: 'Returns a result of the bitwise `or` operation of the two values.' },
|
||||
{ text: 'binary_shift_left', hint: 'Returns binary shift left operation on a pair of numbers.' },
|
||||
{ text: 'binary_shift_right', hint: 'Returns binary shift right operation on a pair of numbers.' },
|
||||
{ text: 'binary_xor', hint: 'Returns a result of the bitwise `xor` operation of the two values.' },
|
||||
{ text: 'buildschema', hint: 'Returns the minimal schema that admits all values of *DynamicExpr*.' },
|
||||
{
|
||||
text: 'case',
|
||||
hint: 'Evaluates a list of predicates and returns the first result expression whose predicate is satisfied.',
|
||||
},
|
||||
{
|
||||
text: 'ceiling',
|
||||
hint: 'Calculates the smallest integer greater than, or equal to, the specified numeric expression.',
|
||||
},
|
||||
{ text: 'cluster', hint: 'Changes the reference of the query to a remote cluster.' },
|
||||
{
|
||||
text: 'coalesce',
|
||||
hint: 'Evaluates a list of expressions and returns the first non-null (or non-empty for string) expression.',
|
||||
},
|
||||
{ text: 'cos', hint: 'Returns the cosine function.' },
|
||||
{ text: 'cot', hint: 'Calculates the trigonometric cotangent of the specified angle, in radians.' },
|
||||
{
|
||||
text: 'count',
|
||||
hint:
|
||||
'Returns a count of the records per summarization group (or in total if summarization is done without grouping).',
|
||||
},
|
||||
{ text: 'countif', hint: 'Returns a count of rows for which *Predicate* evaluates to `true`.' },
|
||||
{
|
||||
text: 'countof',
|
||||
hint: 'Counts occurrences of a substring in a string. Plain string matches may overlap; regex matches do not.',
|
||||
},
|
||||
{ text: 'current_principal', hint: 'Returns the current principal running this query.' },
|
||||
{
|
||||
text: 'cursor_after',
|
||||
hint: 'A predicate over the records of a table to compare their ingestion time\r\nagainst a database cursor.',
|
||||
},
|
||||
{
|
||||
text: 'cursor_before_or_at',
|
||||
hint: 'A predicate over the records of a table to compare their ingestion time\r\nagainst a database cursor.',
|
||||
},
|
||||
{ text: 'database', hint: 'Changes the reference of the query to a specific database within the cluster scope.' },
|
||||
{
|
||||
text: 'datetime_add',
|
||||
hint:
|
||||
'Calculates a new [datetime](./scalar-data-types/datetime.md) from a specified datepart multiplied by a specified amount, added to a specified [datetime](./scalar-data-types/datetime.md).',
|
||||
},
|
||||
{
|
||||
text: 'datetime_diff',
|
||||
hint: 'Calculates calendarian difference between two [datetime](./scalar-data-types/datetime.md) values.',
|
||||
},
|
||||
{ text: 'datetime_part', hint: 'Extracts the requested date part as an integer value.' },
|
||||
{ text: 'dayofmonth', hint: 'Returns the integer number representing the day number of the given month' },
|
||||
{ text: 'dayofweek', hint: 'Returns the integer number of days since the preceding Sunday, as a `timespan`.' },
|
||||
{ text: 'dayofyear', hint: 'Returns the integer number represents the day number of the given year.' },
|
||||
{ text: 'dcount', hint: 'Returns an estimate of the number of distinct values of *Expr* in the group.' },
|
||||
{
|
||||
text: 'dcount_hll',
|
||||
hint:
|
||||
'Calculates the dcount from hll results (which was generated by [hll](hll-aggfunction.md) or [hll_merge](hll-merge-aggfunction.md)).',
|
||||
},
|
||||
{
|
||||
text: 'dcountif',
|
||||
hint:
|
||||
'Returns an estimate of the number of distinct values of *Expr* of rows for which *Predicate* evaluates to `true`.',
|
||||
},
|
||||
{
|
||||
text: 'degrees',
|
||||
hint:
|
||||
'Converts angle value in radians into value in degrees, using formula `degrees = (180 / PI ) * angle_in_radians`',
|
||||
},
|
||||
{ text: 'distance', hint: 'Returns the distance between two points in meters.' },
|
||||
{ text: 'endofday', hint: 'Returns the end of the day containing the date, shifted by an offset, if provided.' },
|
||||
{ text: 'endofmonth', hint: 'Returns the end of the month containing the date, shifted by an offset, if provided.' },
|
||||
{ text: 'endofweek', hint: 'Returns the end of the week containing the date, shifted by an offset, if provided.' },
|
||||
{ text: 'endofyear', hint: 'Returns the end of the year containing the date, shifted by an offset, if provided.' },
|
||||
{
|
||||
text: 'estimate_data_size',
|
||||
hint: 'Returns an estimated data size of the selected columns of the tabular expression.',
|
||||
},
|
||||
{ text: 'exp', hint: 'The base-e exponential function of x, which is e raised to the power x: e^x.' },
|
||||
{
|
||||
text: 'exp10',
|
||||
hint: 'The base-10 exponential function of x, which is 10 raised to the power x: 10^x. \r\n**Syntax**',
|
||||
},
|
||||
{ text: 'exp2', hint: 'The base-2 exponential function of x, which is 2 raised to the power x: 2^x.' },
|
||||
{
|
||||
text: 'extent_id',
|
||||
hint: 'Returns a unique identifier that identifies the data shard ("extent") that the current record resides in.',
|
||||
},
|
||||
{
|
||||
text: 'extent_tags',
|
||||
hint:
|
||||
'Returns a dynamic array with the [tags](../management/extents-overview.md#extent-tagging) of the data shard ("extent") that the current record resides in.',
|
||||
},
|
||||
{ text: 'extract', hint: 'Get a match for a [regular expression](./re2.md) from a text string.' },
|
||||
{ text: 'extract_all', hint: 'Get all matches for a [regular expression](./re2.md) from a text string.' },
|
||||
{ text: 'extractjson', hint: 'Get a specified element out of a JSON text using a path expression.' },
|
||||
{ text: 'floor', hint: 'An alias for [`bin()`](binfunction.md).' },
|
||||
{ text: 'format_datetime', hint: 'Formats a datetime parameter based on the format pattern parameter.' },
|
||||
{ text: 'format_timespan', hint: 'Formats a timespan parameter based on the format pattern parameter.' },
|
||||
{ text: 'gamma', hint: 'Computes [gamma function](https://en.wikipedia.org/wiki/Gamma_function)' },
|
||||
{ text: 'getmonth', hint: 'Get the month number (1-12) from a datetime.' },
|
||||
{ text: 'gettype', hint: 'Returns the runtime type of its single argument.' },
|
||||
{ text: 'getyear', hint: 'Returns the year part of the `datetime` argument.' },
|
||||
{ text: 'hash', hint: 'Returns a hash value for the input value.' },
|
||||
{ text: 'hash_sha256', hint: 'Returns a sha256 hash value for the input value.' },
|
||||
{ text: 'hll', hint: 'Calculates the Intermediate results of [dcount](dcount-aggfunction.md) across the group.' },
|
||||
{
|
||||
text: 'hll_merge',
|
||||
hint: 'Merges hll results (scalar version of the aggregate version [`hll_merge()`](hll-merge-aggfunction.md)).',
|
||||
},
|
||||
{ text: 'hourofday', hint: 'Returns the integer number representing the hour number of the given date' },
|
||||
{
|
||||
text: 'iff',
|
||||
hint:
|
||||
'Evaluates the first argument (the predicate), and returns the value of either the second or third arguments, depending on whether the predicate evaluated to `true` (second) or `false` (third).',
|
||||
},
|
||||
{
|
||||
text: 'iif',
|
||||
hint:
|
||||
'Evaluates the first argument (the predicate), and returns the value of either the second or third arguments, depending on whether the predicate evaluated to `true` (second) or `false` (third).',
|
||||
},
|
||||
{
|
||||
text: 'indexof',
|
||||
hint: 'Function reports the zero-based index of the first occurrence of a specified string within input string.',
|
||||
},
|
||||
{ text: 'ingestion_time', hint: "Retrieves the record's `$IngestionTime` hidden `datetime` column, or null." },
|
||||
{
|
||||
text: 'iscolumnexists',
|
||||
hint:
|
||||
'Returns a boolean value indicating if the given string argument exists in the schema produced by the preceding tabular operator.',
|
||||
},
|
||||
{ text: 'isempty', hint: 'Returns `true` if the argument is an empty string or is null.' },
|
||||
{ text: 'isfinite', hint: 'Returns whether input is a finite value (is neither infinite nor NaN).' },
|
||||
{ text: 'isinf', hint: 'Returns whether input is an infinite (positive or negative) value.' },
|
||||
{ text: 'isnan', hint: 'Returns whether input is Not-a-Number (NaN) value.' },
|
||||
{ text: 'isnotempty', hint: 'Returns `true` if the argument is not an empty string nor it is a null.' },
|
||||
{ text: 'isnotnull', hint: 'Returns `true` if the argument is not null.' },
|
||||
{
|
||||
text: 'isnull',
|
||||
hint:
|
||||
'Evaluates its sole argument and returns a `bool` value indicating if the argument evaluates to a null value.',
|
||||
},
|
||||
{ text: 'log', hint: 'Returns the natural logarithm function.' },
|
||||
{ text: 'log10', hint: 'Returns the common (base-10) logarithm function.' },
|
||||
{ text: 'log2', hint: 'Returns the base-2 logarithm function.' },
|
||||
{
|
||||
text: 'loggamma',
|
||||
hint: 'Computes log of absolute value of the [gamma function](https://en.wikipedia.org/wiki/Gamma_function)',
|
||||
},
|
||||
{
|
||||
text: 'make_datetime',
|
||||
hint: 'Creates a [datetime](./scalar-data-types/datetime.md) scalar value from the specified date and time.',
|
||||
},
|
||||
{
|
||||
text: 'make_dictionary',
|
||||
hint: 'Returns a `dynamic` (JSON) property-bag (dictionary) of all the values of *Expr* in the group.',
|
||||
},
|
||||
{ text: 'make_string', hint: 'Returns the string generated by the Unicode characters.' },
|
||||
{
|
||||
text: 'make_timespan',
|
||||
hint: 'Creates a [timespan](./scalar-data-types/timespan.md) scalar value from the specified time period.',
|
||||
},
|
||||
{ text: 'makelist', hint: 'Returns a `dynamic` (JSON) array of all the values of *Expr* in the group.' },
|
||||
{
|
||||
text: 'makeset',
|
||||
hint: 'Returns a `dynamic` (JSON) array of the set of distinct values that *Expr* takes in the group.',
|
||||
},
|
||||
{
|
||||
text: 'materialize',
|
||||
hint:
|
||||
'Allows caching a sub-query result during the time of query execution in a way that other subqueries can reference the partial result.',
|
||||
},
|
||||
{ text: 'max', hint: 'Returns the maximum value across the group.' },
|
||||
{ text: 'max_of', hint: 'Returns the maximum value of several evaluated numeric expressions.' },
|
||||
{
|
||||
text: 'merge_tdigests',
|
||||
hint:
|
||||
'Merges tdigest results (scalar version of the aggregate version [`merge_tdigests()`](merge-tdigests-aggfunction.md)).',
|
||||
},
|
||||
{ text: 'min', hint: 'Returns the minimum value agross the group.' },
|
||||
{ text: 'min_of', hint: 'Returns the minimum value of several evaluated numeric expressions.' },
|
||||
{ text: 'monthofyear', hint: 'Returns the integer number represents the month number of the given year.' },
|
||||
{
|
||||
text: 'next',
|
||||
hint:
|
||||
'Returns the value of a column in a row that it at some offset following the\r\ncurrent row in a [serialized row set](./windowsfunctions.md#serialized-row-set).',
|
||||
},
|
||||
{ text: 'not', hint: 'Reverses the value of its `bool` argument.' },
|
||||
{
|
||||
text: 'now',
|
||||
hint:
|
||||
'Returns the current UTC clock time, optionally offset by a given timespan.\r\nThis function can be used multiple times in a statement and the clock time being referenced will be the same for all instances.',
|
||||
},
|
||||
{ text: 'pack', hint: 'Creates a `dynamic` object (property bag) from a list of names and values.' },
|
||||
{
|
||||
text: 'pack_all',
|
||||
hint: 'Creates a `dynamic` object (property bag) from all the columns of the tabular expression.',
|
||||
},
|
||||
{ text: 'pack_array', hint: 'Packs all input values into a dynamic array.' },
|
||||
{ text: 'parse_ipv4', hint: 'Converts input to integer (signed 64-bit) number representation.' },
|
||||
{
|
||||
text: 'parse_json',
|
||||
hint:
|
||||
'Interprets a `string` as a [JSON value](https://json.org/)) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md). \r\nIt is superior to using [extractjson() function](./extractjsonfunction.md)\r\nwhen you need to extract more than one element of a JSON compound object.',
|
||||
},
|
||||
{
|
||||
text: 'parse_path',
|
||||
hint:
|
||||
'Parses a file path `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object that contains the following parts of the path: \r\nScheme, RootPath, DirectoryPath, DirectoryName, FileName, Extension, AlternateDataStreamName.\r\nIn addition to the simple paths with both types of slashes, supports paths with schemas (e.g. "file://..."), shared paths (e.g. "\\\\shareddrive\\users..."), long paths (e.g "\\\\?\\C:...""), alternate data streams (e.g. "file1.exe:file2.exe")',
|
||||
},
|
||||
{
|
||||
text: 'parse_url',
|
||||
hint:
|
||||
'Parses an absolute URL `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object contains all parts of the URL (Scheme, Host, Port, Path, Username, Password, Query Parameters, Fragment).',
|
||||
},
|
||||
{
|
||||
text: 'parse_urlquery',
|
||||
hint:
|
||||
'Parses a url query `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object contains the Query parameters.',
|
||||
},
|
||||
{
|
||||
text: 'parse_user_agent',
|
||||
hint:
|
||||
"Interprets a user-agent string, which identifies the user's browser and provides certain system details to servers hosting the websites the user visits. The result is returned as [`dynamic`](./scalar-data-types/dynamic.md).",
|
||||
},
|
||||
{ text: 'parse_version', hint: 'Converts input string representation of version to a comparable decimal number.' },
|
||||
{
|
||||
text: 'parse_xml',
|
||||
hint:
|
||||
'Interprets a `string` as a XML value, converts the value to a [JSON value](https://json.org/) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md).',
|
||||
},
|
||||
{
|
||||
text: 'percentile',
|
||||
hint:
|
||||
'Returns an estimate for the specified [nearest-rank percentile](#nearest-rank-percentile) of the population defined by *Expr*. \r\nThe accuracy depends on the density of population in the region of the percentile.',
|
||||
},
|
||||
{
|
||||
text: 'percentile_tdigest',
|
||||
hint:
|
||||
'Calculates the percentile result from tdigest results (which was generated by [tdigest](tdigest-aggfunction.md) or [merge-tdigests](merge-tdigests-aggfunction.md))',
|
||||
},
|
||||
{
|
||||
text: 'percentrank_tdigest',
|
||||
hint:
|
||||
"Calculates the approximate rank of the value in a set where rank is expressed as percentage of set's size. \r\nThis function can be viewed as the inverse of the percentile.",
|
||||
},
|
||||
{ text: 'pi', hint: 'Returns the constant value of Pi (π).' },
|
||||
{ text: 'point', hint: 'Returns a dynamic array representation of a point.' },
|
||||
{ text: 'pow', hint: 'Returns a result of raising to power' },
|
||||
{
|
||||
text: 'prev',
|
||||
hint:
|
||||
'Returns the value of a column in a row that it at some offset prior to the\r\ncurrent row in a [serialized row set](./windowsfunctions.md#serialized-row-set).',
|
||||
},
|
||||
{
|
||||
text: 'radians',
|
||||
hint:
|
||||
'Converts angle value in degrees into value in radians, using formula `radians = (PI / 180 ) * angle_in_degrees`',
|
||||
},
|
||||
{ text: 'rand', hint: 'Returns a random number.' },
|
||||
{ text: 'range', hint: 'Generates a dynamic array holding a series of equally-spaced values.' },
|
||||
{ text: 'repeat', hint: 'Generates a dynamic array holding a series of equal values.' },
|
||||
{ text: 'replace', hint: 'Replace all regex matches with another string.' },
|
||||
{ text: 'reverse', hint: 'Function makes reverse of input string.' },
|
||||
{ text: 'round', hint: 'Returns the rounded source to the specified precision.' },
|
||||
{
|
||||
text: 'row_cumsum',
|
||||
hint:
|
||||
'Calculates the cumulative sum of a column in a [serialized row set](./windowsfunctions.md#serialized-row-set).',
|
||||
},
|
||||
{
|
||||
text: 'row_number',
|
||||
hint:
|
||||
"Returns the current row's index in a [serialized row set](./windowsfunctions.md#serialized-row-set).\r\nThe row index starts by default at `1` for the first row, and is incremented by `1` for each additional row.\r\nOptionally, the row index can start at a different value than `1`.\r\nAdditionally, the row index may be reset according to some provided predicate.",
|
||||
},
|
||||
{ text: 'series_add', hint: 'Calculates the element-wise addition of two numeric series inputs.' },
|
||||
{ text: 'series_decompose', hint: 'Applies a decomposition transformation on a series.' },
|
||||
{
|
||||
text: 'series_decompose_anomalies',
|
||||
hint:
|
||||
'Anomaly Detection based on series decomposition (refer to [series_decompose()](series-decomposefunction.md))',
|
||||
},
|
||||
{ text: 'series_decompose_forecast', hint: 'Forecast based on series decomposition.' },
|
||||
{ text: 'series_divide', hint: 'Calculates the element-wise division of two numeric series inputs.' },
|
||||
{
|
||||
text: 'series_equals',
|
||||
hint: 'Calculates the element-wise equals (`==`) logic operation of two numeric series inputs.',
|
||||
},
|
||||
{ text: 'series_fill_backward', hint: 'Performs backward fill interpolation of missing values in a series.' },
|
||||
{ text: 'series_fill_const', hint: 'Replaces missing values in a series with a specified constant value.' },
|
||||
{ text: 'series_fill_forward', hint: 'Performs forward fill interpolation of missing values in a series.' },
|
||||
{ text: 'series_fill_linear', hint: 'Performs linear interpolation of missing values in a series.' },
|
||||
{ text: 'series_fir', hint: 'Applies a Finite Impulse Response filter on a series.' },
|
||||
{
|
||||
text: 'series_fit_2lines',
|
||||
hint: 'Applies two segments linear regression on a series, returning multiple columns.',
|
||||
},
|
||||
{
|
||||
text: 'series_fit_2lines_dynamic',
|
||||
hint: 'Applies two segments linear regression on a series, returning dynamic object.',
|
||||
},
|
||||
{ text: 'series_fit_line', hint: 'Applies linear regression on a series, returning multiple columns.' },
|
||||
{ text: 'series_fit_line_dynamic', hint: 'Applies linear regression on a series, returning dynamic object.' },
|
||||
{
|
||||
text: 'series_greater',
|
||||
hint: 'Calculates the element-wise greater (`>`) logic operation of two numeric series inputs.',
|
||||
},
|
||||
{
|
||||
text: 'series_greater_equals',
|
||||
hint: 'Calculates the element-wise greater or equals (`>=`) logic operation of two numeric series inputs.',
|
||||
},
|
||||
{ text: 'series_iir', hint: 'Applies a Infinite Impulse Response filter on a series.' },
|
||||
{ text: 'series_less', hint: 'Calculates the element-wise less (`<`) logic operation of two numeric series inputs.' },
|
||||
{
|
||||
text: 'series_less_equals',
|
||||
hint: 'Calculates the element-wise less or equal (`<=`) logic operation of two numeric series inputs.',
|
||||
},
|
||||
{ text: 'series_multiply', hint: 'Calculates the element-wise multiplication of two numeric series inputs.' },
|
||||
{
|
||||
text: 'series_not_equals',
|
||||
hint: 'Calculates the element-wise not equals (`!=`) logic operation of two numeric series inputs.',
|
||||
},
|
||||
{ text: 'series_outliers', hint: 'Scores anomaly points in a series.' },
|
||||
{ text: 'series_periods_detect', hint: 'Finds the most significant periods that exist in a time series.' },
|
||||
{
|
||||
text: 'series_periods_validate',
|
||||
hint: 'Checks whether a time series contains periodic patterns of given lengths.',
|
||||
},
|
||||
{
|
||||
text: 'series_seasonal',
|
||||
hint: 'Calculates the seasonal component of a series according to the detected or given seasonal period.',
|
||||
},
|
||||
{ text: 'series_stats', hint: 'Returns statistics for a series in multiple columns.' },
|
||||
{ text: 'series_stats_dynamic', hint: 'Returns statistics for a series in dynamic object.' },
|
||||
{ text: 'series_subtract', hint: 'Calculates the element-wise subtraction of two numeric series inputs.' },
|
||||
{ text: 'sign', hint: 'Sign of a numeric expression' },
|
||||
{ text: 'sin', hint: 'Returns the sine function.' },
|
||||
{
|
||||
text: 'split',
|
||||
hint:
|
||||
'Splits a given string according to a given delimiter and returns a string array with the contained substrings.',
|
||||
},
|
||||
{ text: 'sqrt', hint: 'Returns the square root function.' },
|
||||
{ text: 'startofday', hint: 'Returns the start of the day containing the date, shifted by an offset, if provided.' },
|
||||
{
|
||||
text: 'startofmonth',
|
||||
hint: 'Returns the start of the month containing the date, shifted by an offset, if provided.',
|
||||
},
|
||||
{
|
||||
text: 'startofweek',
|
||||
hint: 'Returns the start of the week containing the date, shifted by an offset, if provided.',
|
||||
},
|
||||
{
|
||||
text: 'startofyear',
|
||||
hint: 'Returns the start of the year containing the date, shifted by an offset, if provided.',
|
||||
},
|
||||
{
|
||||
text: 'stdev',
|
||||
hint:
|
||||
'Calculates the standard deviation of *Expr* across the group, considering the group as a [sample](https://en.wikipedia.org/wiki/Sample_%28statistics%29).',
|
||||
},
|
||||
{
|
||||
text: 'stdevif',
|
||||
hint:
|
||||
'Calculates the [stdev](stdev-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`.',
|
||||
},
|
||||
{
|
||||
text: 'stdevp',
|
||||
hint:
|
||||
'Calculates the standard deviation of *Expr* across the group, considering the group as a [population](https://en.wikipedia.org/wiki/Statistical_population).',
|
||||
},
|
||||
{ text: 'strcat', hint: 'Concatenates between 1 and 64 arguments.' },
|
||||
{ text: 'strcat_array', hint: 'Creates a concatenated string of array values using specified delimiter.' },
|
||||
{
|
||||
text: 'strcat_delim',
|
||||
hint: 'Concatenates between 2 and 64 arguments, with delimiter, provided as first argument.',
|
||||
},
|
||||
{ text: 'strcmp', hint: 'Compares two strings.' },
|
||||
{ text: 'string_size', hint: 'Returns the size, in bytes, of the input string.' },
|
||||
{ text: 'strlen', hint: 'Returns the length, in characters, of the input string.' },
|
||||
{ text: 'strrep', hint: 'Repeats given [string](./scalar-data-types/string.md) provided amount of times.' },
|
||||
{
|
||||
text: 'substring',
|
||||
hint: 'Extracts a substring from a source string starting from some index to the end of the string.',
|
||||
},
|
||||
{ text: 'sum', hint: 'Calculates the sum of *Expr* across the group.' },
|
||||
{ text: 'sumif', hint: 'Returns a sum of *Expr* for which *Predicate* evaluates to `true`.' },
|
||||
{ text: 'table', hint: 'References specific table using an query-time evaluated string-expression.' },
|
||||
{ text: 'tan', hint: 'Returns the tangent function.' },
|
||||
{
|
||||
text: 'tdigest',
|
||||
hint: 'Calculates the Intermediate results of [`percentiles()`](percentiles-aggfunction.md) across the group.',
|
||||
},
|
||||
{
|
||||
text: 'tdigest_merge',
|
||||
hint:
|
||||
'Merges tdigest results (scalar version of the aggregate version [`tdigest_merge()`](tdigest-merge-aggfunction.md)).',
|
||||
},
|
||||
{ text: 'tobool', hint: 'Converts input to boolean (signed 8-bit) representation.' },
|
||||
{ text: 'todatetime', hint: 'Converts input to [datetime](./scalar-data-types/datetime.md) scalar.' },
|
||||
{ text: 'todecimal', hint: 'Converts input to decimal number representation.' },
|
||||
{
|
||||
text: 'todouble',
|
||||
hint: 'Converts the input to a value of type `real`. (`todouble()` and `toreal()` are synonyms.)',
|
||||
},
|
||||
{
|
||||
text: 'todynamic',
|
||||
hint:
|
||||
'Interprets a `string` as a [JSON value](https://json.org/) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md).',
|
||||
},
|
||||
{ text: 'toguid', hint: 'Converts input to [`guid`](./scalar-data-types/guid.md) representation.' },
|
||||
{ text: 'tohex', hint: 'Converts input to a hexadecimal string.' },
|
||||
{ text: 'toint', hint: 'Converts input to integer (signed 32-bit) number representation.' },
|
||||
{ text: 'tolong', hint: 'Converts input to long (signed 64-bit) number representation.' },
|
||||
{ text: 'tolower', hint: 'Converts input string to lower case.' },
|
||||
{ text: 'toscalar', hint: 'Returns a scalar constant value of the evaluated expression.' },
|
||||
{ text: 'tostring', hint: 'Converts input to a string representation.' },
|
||||
{ text: 'totimespan', hint: 'Converts input to [timespan](./scalar-data-types/timespan.md) scalar.' },
|
||||
{ text: 'toupper', hint: 'Converts a string to upper case.' },
|
||||
{
|
||||
text: 'translate',
|
||||
hint:
|
||||
"Replaces a set of characters ('searchList') with another set of characters ('replacementList') in a given a string.\r\nThe function searches for characters in the 'searchList' and replaces them with the corresponding characters in 'replacementList'",
|
||||
},
|
||||
{ text: 'treepath', hint: 'Enumerates all the path expressions that identify leaves in a dynamic object.' },
|
||||
{ text: 'trim', hint: 'Removes all leading and trailing matches of the specified regular expression.' },
|
||||
{ text: 'trim_end', hint: 'Removes trailing match of the specified regular expression.' },
|
||||
{ text: 'trim_start', hint: 'Removes leading match of the specified regular expression.' },
|
||||
{ text: 'url_decode', hint: 'The function converts encoded URL into a to regular URL representation.' },
|
||||
{
|
||||
text: 'url_encode',
|
||||
hint: 'The function converts characters of the input URL into a format that can be transmitted over the Internet.',
|
||||
},
|
||||
{
|
||||
text: 'variance',
|
||||
hint:
|
||||
'Calculates the variance of *Expr* across the group, considering the group as a [sample](https://en.wikipedia.org/wiki/Sample_%28statistics%29).',
|
||||
},
|
||||
{
|
||||
text: 'varianceif',
|
||||
hint:
|
||||
'Calculates the [variance](variance-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`.',
|
||||
},
|
||||
{
|
||||
text: 'variancep',
|
||||
hint:
|
||||
'Calculates the variance of *Expr* across the group, considering the group as a [population](https://en.wikipedia.org/wiki/Statistical_population).',
|
||||
},
|
||||
{ text: 'weekofyear', hint: 'Returns the integer number represents the week number.' },
|
||||
{
|
||||
text: 'welch_test',
|
||||
hint: 'Computes the p_value of the [Welch-test function](https://en.wikipedia.org/wiki/Welch%27s_t-test)',
|
||||
},
|
||||
{
|
||||
text: 'zip',
|
||||
hint:
|
||||
'The `zip` function accepts any number of `dynamic` arrays, and returns an\r\narray whose elements are each an array holding the elements of the input\r\narrays of the same index.',
|
||||
},
|
||||
];
|
||||
|
||||
export const KEYWORDS = [
|
||||
'by',
|
||||
'on',
|
||||
'contains',
|
||||
'notcontains',
|
||||
'containscs',
|
||||
'notcontainscs',
|
||||
'startswith',
|
||||
'has',
|
||||
'matches',
|
||||
'regex',
|
||||
'true',
|
||||
'false',
|
||||
'and',
|
||||
'or',
|
||||
'typeof',
|
||||
'int',
|
||||
'string',
|
||||
'date',
|
||||
'datetime',
|
||||
'time',
|
||||
'long',
|
||||
'real',
|
||||
'boolean',
|
||||
'bool',
|
||||
];
|
||||
|
||||
export const grafanaMacros = [
|
||||
{
|
||||
text: '$__timeFilter',
|
||||
display: '$__timeFilter()',
|
||||
hint: 'Macro that uses the selected timerange in Grafana to filter the query.',
|
||||
},
|
||||
{
|
||||
text: '$__timeTo',
|
||||
display: '$__timeTo()',
|
||||
hint: 'Returns the From datetime from the Grafana picker. Example: datetime(2018-06-05T20:09:58.907Z).',
|
||||
},
|
||||
{
|
||||
text: '$__timeFrom',
|
||||
display: '$__timeFrom()',
|
||||
hint: 'Returns the From datetime from the Grafana picker. Example: datetime(2018-06-05T18:09:58.907Z).',
|
||||
},
|
||||
{
|
||||
text: '$__escapeMulti',
|
||||
display: '$__escapeMulti()',
|
||||
hint: 'Macro to escape multi-value template variables that contain illegal characters.',
|
||||
},
|
||||
{ text: '$__contains', display: '$__contains()', hint: 'Macro for multi-value template variables.' },
|
||||
];
|
||||
|
||||
// Kusto operators
|
||||
// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~'];
|
||||
|
||||
export const DURATION = ['SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS'];
|
||||
|
||||
const tokenizer = {
|
||||
comment: {
|
||||
pattern: /(^|[^\\:])\/\/.*/,
|
||||
lookbehind: true,
|
||||
greedy: true,
|
||||
},
|
||||
'function-context': {
|
||||
pattern: /[a-z0-9_]+\([^)]*\)?/i,
|
||||
inside: {},
|
||||
},
|
||||
duration: {
|
||||
pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'),
|
||||
alias: 'number',
|
||||
},
|
||||
builtin: new RegExp(`\\b(?:${functionTokens.map((f) => f.text).join('|')})(?=\\s*\\()`, 'i'),
|
||||
string: {
|
||||
pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
|
||||
greedy: true,
|
||||
},
|
||||
keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|${operatorTokens.map((f) => f.text).join('|')}|\\*)\\b`, 'i'),
|
||||
boolean: /\b(?:true|false)\b/,
|
||||
number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
|
||||
operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/,
|
||||
punctuation: /[{};(),.:]/,
|
||||
variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/,
|
||||
};
|
||||
|
||||
tokenizer['function-context'].inside = {
|
||||
argument: {
|
||||
pattern: /[a-z0-9_]+(?=:)/i,
|
||||
alias: 'symbol',
|
||||
},
|
||||
duration: tokenizer.duration,
|
||||
number: tokenizer.number,
|
||||
builtin: tokenizer.builtin,
|
||||
string: tokenizer.string,
|
||||
variable: tokenizer.variable,
|
||||
};
|
||||
|
||||
// console.log(tokenizer.builtin);
|
||||
|
||||
export default tokenizer;
|
||||
|
||||
// function escapeRegExp(str: string): string {
|
||||
// return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
|
||||
// }
|
@ -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
Block a user