-
-
-
Folder Settings
+
+
+
-
+
+
);
}
}
diff --git a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap
index 2de0c193d27..9ffd87e13d5 100644
--- a/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap
+++ b/public/app/features/folders/__snapshots__/FolderSettingsPage.test.tsx.snap
@@ -1,131 +1,137 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should enable save button 1`] = `
-
-
-
+
-
- Folder Settings
-
-
-
+
+
`;
exports[`Render should render component 1`] = `
-
-
-
+
-
- Folder Settings
-
-
-
+
+
-
- Name
-
-
-
-
-
-
+ Name
+
+
- Save
-
-
+
-
- Delete
-
-
-
+
+
+ Save
+
+
+
+ Delete
+
+
+
+
-
-
+
+
`;
diff --git a/public/app/features/panel/GeneralTabCtrl.ts b/public/app/features/panel/GeneralTabCtrl.ts
index 75edd72027b..9db13713ce1 100644
--- a/public/app/features/panel/GeneralTabCtrl.ts
+++ b/public/app/features/panel/GeneralTabCtrl.ts
@@ -24,7 +24,7 @@ export class GeneralTabCtrl {
const panelPropsString = panelPropsToTrack
.map(prop => prop + '=' + (panel[prop] && panel[prop].toString ? panel[prop].toString() : panel[prop]))
.join();
- const panelLinks = panel.links;
+ const panelLinks = panel.links || [];
const panelLinksString = panelLinks.map(obj2string).join();
return panelPropsString + panelLinksString;
};
diff --git a/public/app/features/panel/all.ts b/public/app/features/panel/all.ts
index 8bc2822a77c..9fb855197f3 100644
--- a/public/app/features/panel/all.ts
+++ b/public/app/features/panel/all.ts
@@ -1,6 +1,5 @@
import './panel_header';
import './panel_directive';
-import './solo_panel_ctrl';
import './query_ctrl';
import './panel_editor_tab';
import './query_editor_row';
diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts
index 0b3bbc3080f..ceebfd82335 100644
--- a/public/app/features/panel/metrics_panel_ctrl.ts
+++ b/public/app/features/panel/metrics_panel_ctrl.ts
@@ -16,7 +16,6 @@ class MetricsPanelCtrl extends PanelCtrl {
datasourceSrv: any;
timeSrv: any;
templateSrv: any;
- timing: any;
range: any;
interval: any;
intervalMs: any;
@@ -81,7 +80,6 @@ class MetricsPanelCtrl extends PanelCtrl {
this.loading = true;
// load datasource service
- this.setTimeQueryStart();
this.datasourceSrv
.get(this.panel.datasource)
.then(this.updateTimeRange.bind(this))
@@ -112,14 +110,6 @@ class MetricsPanelCtrl extends PanelCtrl {
});
}
- setTimeQueryStart() {
- this.timing.queryStart = new Date().getTime();
- }
-
- setTimeQueryEnd() {
- this.timing.queryEnd = new Date().getTime();
- }
-
updateTimeRange(datasource?) {
this.datasource = datasource || this.datasource;
this.range = this.timeSrv.timeRange();
@@ -181,7 +171,6 @@ class MetricsPanelCtrl extends PanelCtrl {
}
handleQueryResult(result) {
- this.setTimeQueryEnd();
this.loading = false;
// check for if data source returns subject
diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts
index 2f1ef72cedd..6799b209147 100644
--- a/public/app/features/panel/panel_ctrl.ts
+++ b/public/app/features/panel/panel_ctrl.ts
@@ -1,5 +1,4 @@
import _ from 'lodash';
-import $ from 'jquery';
import Remarkable from 'remarkable';
import config from 'app/core/config';
@@ -13,7 +12,7 @@ import {
sharePanel as sharePanelUtil,
} from 'app/features/dashboard/utils/panel';
-import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
+import { GRID_COLUMN_COUNT, PANEL_HEADER_HEIGHT, PANEL_BORDER } from 'app/core/constants';
export class PanelCtrl {
panel: any;
@@ -31,8 +30,8 @@ export class PanelCtrl {
height: any;
containerHeight: any;
events: Emitter;
- timing: any;
loading: boolean;
+ timing: any;
maxPanelsPerRowOptions: number[];
constructor($scope, $injector) {
@@ -42,7 +41,7 @@ export class PanelCtrl {
this.$timeout = $injector.get('$timeout');
this.editorTabs = [];
this.events = this.panel.events;
- this.timing = {};
+ this.timing = {}; // not used but here to not break plugins
const plugin = config.panels[this.panel.type];
if (plugin) {
@@ -59,7 +58,7 @@ export class PanelCtrl {
}
renderingCompleted() {
- profiler.renderingCompleted(this.panel.id, this.timing);
+ profiler.renderingCompleted(this.panel.id);
}
refresh() {
@@ -200,24 +199,12 @@ export class PanelCtrl {
return this.dashboard.meta.fullscreen && !this.panel.fullscreen;
}
- calculatePanelHeight() {
- if (this.panel.isEditing) {
- this.containerHeight = $('.panel-wrapper--edit').height();
- } else if (this.panel.fullscreen) {
- this.containerHeight = $('.panel-wrapper--view').height();
- } else {
- this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN;
- }
-
- if (this.panel.soloMode) {
- this.containerHeight = $(window).height();
- }
-
+ calculatePanelHeight(containerHeight) {
+ this.containerHeight = containerHeight;
this.height = this.containerHeight - (PANEL_BORDER + PANEL_HEADER_HEIGHT);
}
render(payload?) {
- this.timing.renderStart = new Date().getTime();
this.events.emit('render', payload);
}
diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts
index 1fd0b129720..31da05a2d4f 100644
--- a/public/app/features/panel/panel_directive.ts
+++ b/public/app/features/panel/panel_directive.ts
@@ -101,7 +101,7 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
});
ctrl.events.on('panel-size-changed', () => {
- ctrl.calculatePanelHeight();
+ ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
$timeout(() => {
resizeScrollableContent();
ctrl.render();
@@ -112,19 +112,21 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => {
// first wait one pass for dashboard fullscreen view mode to take effect (classses being applied)
setTimeout(() => {
// then recalc style
- ctrl.calculatePanelHeight();
+ ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
// then wait another cycle (this might not be needed)
$timeout(() => {
ctrl.render();
resizeScrollableContent();
});
- });
+ }, 10);
});
- // set initial height
- ctrl.calculatePanelHeight();
-
ctrl.events.on('render', () => {
+ // set initial height
+ if (!ctrl.height) {
+ ctrl.calculatePanelHeight(panelContainer[0].offsetHeight);
+ }
+
if (transparentLastState !== ctrl.panel.transparent) {
panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
transparentLastState = ctrl.panel.transparent;
diff --git a/public/app/features/panel/panellinks/module.ts b/public/app/features/panel/panellinks/module.ts
index c10e4ab224e..99a6975811e 100644
--- a/public/app/features/panel/panellinks/module.ts
+++ b/public/app/features/panel/panellinks/module.ts
@@ -9,7 +9,7 @@ function panelLinksEditor() {
},
restrict: 'E',
controller: 'PanelLinksEditorCtrl',
- templateUrl: 'public/app/features/dashboard/panellinks/module.html',
+ templateUrl: 'public/app/features/panel/panellinks/module.html',
link: () => {},
};
}
diff --git a/public/app/features/panel/partials/soloPanel.html b/public/app/features/panel/partials/soloPanel.html
deleted file mode 100644
index 644bbe74ffb..00000000000
--- a/public/app/features/panel/partials/soloPanel.html
+++ /dev/null
@@ -1,4 +0,0 @@
-
diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts
deleted file mode 100644
index a8bf5371913..00000000000
--- a/public/app/features/panel/solo_panel_ctrl.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import angular from 'angular';
-import locationUtil from 'app/core/utils/location_util';
-import appEvents from 'app/core/app_events';
-
-export class SoloPanelCtrl {
- /** @ngInject */
- constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv, backendSrv) {
- let panelId;
-
- $scope.init = () => {
- contextSrv.sidemenu = false;
- appEvents.emit('toggle-sidemenu-hidden');
-
- const params = $location.search();
- panelId = parseInt(params.panelId, 10);
-
- appEvents.on('dashboard-initialized', $scope.initPanelScope);
-
- // if no uid, redirect to new route based on slug
- if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
- backendSrv.getDashboardBySlug($routeParams.slug).then(res => {
- if (res) {
- const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
- $location.path(url).replace();
- }
- });
- return;
- }
-
- dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug, $routeParams.uid).then(result => {
- result.meta.soloMode = true;
- $scope.initDashboard(result, $scope);
- });
- };
-
- $scope.initPanelScope = () => {
- const panelInfo = $scope.dashboard.getPanelInfoById(panelId);
-
- // fake row ctrl scope
- $scope.ctrl = {
- dashboard: $scope.dashboard,
- };
-
- $scope.panel = panelInfo.panel;
- $scope.panel.soloMode = true;
- $scope.$index = 0;
-
- if (!$scope.panel) {
- $scope.appEvent('alert-error', ['Panel not found', '']);
- return;
- }
- };
-
- $scope.init();
- }
-}
-
-angular.module('grafana.routes').controller('SoloPanelCtrl', SoloPanelCtrl);
diff --git a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts
index 8b9607d39ad..d647af616a9 100644
--- a/public/app/features/panel/specs/metrics_panel_ctrl.test.ts
+++ b/public/app/features/panel/specs/metrics_panel_ctrl.test.ts
@@ -11,7 +11,7 @@ jest.mock('app/core/config', () => {
});
import q from 'q';
-import { PanelModel } from 'app/features/dashboard/panel_model';
+import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
describe('MetricsPanelCtrl', () => {
diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts
index c4621fda289..078443b019a 100644
--- a/public/app/features/plugins/built_in_plugins.ts
+++ b/public/app/features/plugins/built_in_plugins.ts
@@ -12,6 +12,7 @@ import * as prometheusPlugin from 'app/plugins/datasource/prometheus/module';
import * as mssqlPlugin from 'app/plugins/datasource/mssql/module';
import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module';
import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module';
+import * as azureMonitorPlugin from 'app/plugins/datasource/grafana-azure-monitor-datasource/module';
import * as textPanel from 'app/plugins/panel/text/module';
import * as text2Panel from 'app/plugins/panel/text2/module';
@@ -41,6 +42,7 @@ const builtInPlugins = {
'app/plugins/datasource/prometheus/module': prometheusPlugin,
'app/plugins/datasource/testdata/module': testDataDSPlugin,
'app/plugins/datasource/stackdriver/module': stackdriverPlugin,
+ 'app/plugins/datasource/grafana-azure-monitor-datasource/module': azureMonitorPlugin,
'app/plugins/panel/text/module': textPanel,
'app/plugins/panel/text2/module': text2Panel,
diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts
index 9f53f3c1fd0..a60bb6029b9 100644
--- a/public/app/features/plugins/state/actions.ts
+++ b/public/app/features/plugins/state/actions.ts
@@ -7,6 +7,7 @@ import { PluginDashboard } from '../../../types/plugins';
export enum ActionTypes {
LoadPlugins = 'LOAD_PLUGINS',
LoadPluginDashboards = 'LOAD_PLUGIN_DASHBOARDS',
+ LoadedPluginDashboards = 'LOADED_PLUGIN_DASHBOARDS',
SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY',
SetLayoutMode = 'SET_LAYOUT_MODE',
}
@@ -18,6 +19,10 @@ export interface LoadPluginsAction {
export interface LoadPluginDashboardsAction {
type: ActionTypes.LoadPluginDashboards;
+}
+
+export interface LoadedPluginDashboardsAction {
+ type: ActionTypes.LoadedPluginDashboards;
payload: PluginDashboard[];
}
@@ -46,12 +51,20 @@ const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({
payload: plugins,
});
-const pluginDashboardsLoaded = (dashboards: PluginDashboard[]): LoadPluginDashboardsAction => ({
+const pluginDashboardsLoad = (): LoadPluginDashboardsAction => ({
type: ActionTypes.LoadPluginDashboards,
+});
+
+const pluginDashboardsLoaded = (dashboards: PluginDashboard[]): LoadedPluginDashboardsAction => ({
+ type: ActionTypes.LoadedPluginDashboards,
payload: dashboards,
});
-export type Action = LoadPluginsAction | LoadPluginDashboardsAction | SetPluginsSearchQueryAction | SetLayoutModeAction;
+export type Action = LoadPluginsAction
+ | LoadPluginDashboardsAction
+ | LoadedPluginDashboardsAction
+ | SetPluginsSearchQueryAction
+ | SetLayoutModeAction;
type ThunkResult
= ThunkAction;
@@ -64,8 +77,8 @@ export function loadPlugins(): ThunkResult {
export function loadPluginDashboards(): ThunkResult {
return async (dispatch, getStore) => {
+ dispatch(pluginDashboardsLoad());
const dataSourceType = getStore().dataSources.dataSource.type;
-
const response = await getBackendSrv().get(`api/plugins/${dataSourceType}/dashboards`);
dispatch(pluginDashboardsLoaded(response));
};
diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts
index 0d464ef1772..842775370f5 100644
--- a/public/app/features/plugins/state/reducers.ts
+++ b/public/app/features/plugins/state/reducers.ts
@@ -9,6 +9,7 @@ export const initialState: PluginsState = {
layoutMode: LayoutModes.Grid,
hasFetched: false,
dashboards: [] as PluginDashboard[],
+ isLoadingPluginDashboards: false
};
export const pluginsReducer = (state = initialState, action: Action): PluginsState => {
@@ -23,7 +24,10 @@ export const pluginsReducer = (state = initialState, action: Action): PluginsSta
return { ...state, layoutMode: action.payload };
case ActionTypes.LoadPluginDashboards:
- return { ...state, dashboards: action.payload };
+ return { ...state, dashboards: [], isLoadingPluginDashboards: true };
+
+ case ActionTypes.LoadedPluginDashboards:
+ return { ...state, dashboards: action.payload, isLoadingPluginDashboards: false };
}
return state;
};
diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx
index 38ba23262ba..ebbde595601 100644
--- a/public/app/features/teams/TeamPages.tsx
+++ b/public/app/features/teams/TeamPages.tsx
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import _ from 'lodash';
import { hot } from 'react-hot-loader';
import config from 'app/core/config';
-import PageHeader from 'app/core/components/PageHeader/PageHeader';
+import Page from 'app/core/components/Page/Page';
import TeamMembers from './TeamMembers';
import TeamSettings from './TeamSettings';
import TeamGroupSync from './TeamGroupSync';
@@ -24,6 +24,7 @@ export interface Props {
interface State {
isSyncEnabled: boolean;
+ isLoading: boolean;
}
enum PageTypes {
@@ -33,10 +34,11 @@ enum PageTypes {
}
export class TeamPages extends PureComponent {
- constructor(props) {
+ constructor(props: Props) {
super(props);
this.state = {
+ isLoading: false,
isSyncEnabled: config.buildInfo.isEnterprise,
};
}
@@ -47,8 +49,10 @@ export class TeamPages extends PureComponent {
async fetchTeam() {
const { loadTeam, teamId } = this.props;
-
- return await loadTeam(teamId);
+ this.setState({isLoading: true});
+ const team = await loadTeam(teamId);
+ this.setState({isLoading: false});
+ return team;
}
getCurrentPage() {
@@ -78,10 +82,11 @@ export class TeamPages extends PureComponent {
const { team, navModel } = this.props;
return (
-
-
- {team && Object.keys(team).length !== 0 &&
{this.renderPage()}
}
-
+
+
+ {team && Object.keys(team).length !== 0 && {this.renderPage()}
}
+
+
);
}
}
diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
index f966c52983d..0c09eb3f82d 100644
--- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
+++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap
@@ -1,50 +1,61 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
-
+
`;
exports[`Render should render group sync page 1`] = `
-
+
+
+
+
+
`;
exports[`Render should render member page if team not empty 1`] = `
-
+
+
+
+
+
`;
exports[`Render should render settings and preferences page 1`] = `
-
+
+
+
+
+
`;
diff --git a/public/app/features/templating/editor_ctrl.ts b/public/app/features/templating/editor_ctrl.ts
index fdab5587b42..c47c42a4aad 100644
--- a/public/app/features/templating/editor_ctrl.ts
+++ b/public/app/features/templating/editor_ctrl.ts
@@ -135,7 +135,7 @@ export class VariableEditorCtrl {
$scope.runQuery().then(() => {
$scope.reset();
$scope.mode = 'list';
- templateSrv.updateTemplateData();
+ templateSrv.updateIndex();
});
}
};
diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts
index 30faffea3be..9a041f42cd3 100644
--- a/public/app/features/templating/specs/template_srv.test.ts
+++ b/public/app/features/templating/specs/template_srv.test.ts
@@ -348,7 +348,7 @@ describe('templateSrv', () => {
});
});
- describe('updateTemplateData with simple value', () => {
+ describe('updateIndex with simple value', () => {
beforeEach(() => {
initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]);
});
@@ -476,7 +476,7 @@ describe('templateSrv', () => {
}
]);
_templateSrv.setGrafanaVariable('$__auto_interval_interval', '13m');
- _templateSrv.updateTemplateData();
+ _templateSrv.updateIndex();
});
it('should replace with text except for grafanaVariables', () => {
diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts
index 47522073fb2..db42df7f516 100644
--- a/public/app/features/templating/specs/variable_srv.test.ts
+++ b/public/app/features/templating/specs/variable_srv.test.ts
@@ -1,6 +1,6 @@
import '../all';
import { VariableSrv } from '../variable_srv';
-import { DashboardModel } from '../../dashboard/dashboard_model';
+import { DashboardModel } from '../../dashboard/state/DashboardModel';
import moment from 'moment';
import $q from 'q';
@@ -8,7 +8,9 @@ describe('VariableSrv', function(this: any) {
const ctx = {
datasourceSrv: {},
timeSrv: {
- timeRange: () => {},
+ timeRange: () => {
+ return { from: '2018-01-29', to: '2019-01-29' };
+ },
},
$rootScope: {
$on: () => {},
@@ -21,7 +23,7 @@ describe('VariableSrv', function(this: any) {
init: vars => {
this.variables = vars;
},
- updateTemplateData: () => {},
+ updateIndex: () => {},
replace: str =>
str.replace(this.regex, match => {
return match;
@@ -45,7 +47,14 @@ describe('VariableSrv', function(this: any) {
const ds: any = {};
ds.metricFindQuery = () => Promise.resolve(scenario.queryResult);
- ctx.variableSrv = new VariableSrv(ctx.$rootScope, $q, ctx.$location, ctx.$injector, ctx.templateSrv);
+ ctx.variableSrv = new VariableSrv(
+ ctx.$rootScope,
+ $q,
+ ctx.$location,
+ ctx.$injector,
+ ctx.templateSrv,
+ ctx.timeSrv
+ );
ctx.variableSrv.timeSrv = ctx.timeSrv;
ctx.datasourceSrv = {
diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts
index 0df6209bfdc..b8cabf711ac 100644
--- a/public/app/features/templating/specs/variable_srv_init.test.ts
+++ b/public/app/features/templating/specs/variable_srv_init.test.ts
@@ -2,7 +2,7 @@ import '../all';
import _ from 'lodash';
import { VariableSrv } from '../variable_srv';
-import { DashboardModel } from '../../dashboard/dashboard_model';
+import { DashboardModel } from '../../dashboard/state/DashboardModel';
import $q from 'q';
describe('VariableSrv init', function(this: any) {
@@ -11,13 +11,19 @@ describe('VariableSrv init', function(this: any) {
this.variables = vars;
},
variableInitialized: () => {},
- updateTemplateData: () => {},
+ updateIndex: () => {},
replace: str =>
str.replace(this.regex, match => {
return match;
}),
};
+ const timeSrv = {
+ timeRange: () => {
+ return { from: '2018-01-29', to: '2019-01-29' };
+ },
+ };
+
const $injector = {} as any;
const $rootscope = {
$on: () => {},
@@ -47,7 +53,8 @@ describe('VariableSrv init', function(this: any) {
templateSrv,
};
- ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv);
+ // @ts-ignore
+ ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv, timeSrv);
$injector.instantiate = (variable, model) => {
return getVarMockConstructor(variable, model, ctx);
diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts
index 2f8068137e5..de5efabcf23 100644
--- a/public/app/features/templating/template_srv.ts
+++ b/public/app/features/templating/template_srv.ts
@@ -1,6 +1,7 @@
import kbn from 'app/core/utils/kbn';
import _ from 'lodash';
import { variableRegex } from 'app/features/templating/variable';
+import { TimeRange } from '@grafana/ui/src';
function luceneEscape(value) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
@@ -13,6 +14,7 @@ export class TemplateSrv {
private index = {};
private grafanaVariables = {};
private builtIns = {};
+ private timeRange: TimeRange = null;
constructor() {
this.builtIns['__interval'] = { text: '1s', value: '1s' };
@@ -20,12 +22,13 @@ export class TemplateSrv {
this.variables = [];
}
- init(variables) {
+ init(variables, timeRange?: TimeRange) {
this.variables = variables;
- this.updateTemplateData();
+ this.timeRange = timeRange;
+ this.updateIndex();
}
- updateTemplateData() {
+ updateIndex() {
const existsOrEmpty = value => value || value === '';
this.index = this.variables.reduce((acc, currentValue) => {
@@ -34,6 +37,26 @@ export class TemplateSrv {
}
return acc;
}, {});
+
+ if (this.timeRange) {
+ const from = this.timeRange.from.valueOf().toString();
+ const to = this.timeRange.to.valueOf().toString();
+
+ this.index = {
+ ...this.index,
+ ['__from']: {
+ current: { value: from, text: from },
+ },
+ ['__to']: {
+ current: { value: to, text: to },
+ },
+ };
+ }
+ }
+
+ updateTimeRange(timeRange: TimeRange) {
+ this.timeRange = timeRange;
+ this.updateIndex();
}
variableInitialized(variable) {
@@ -81,8 +104,14 @@ export class TemplateSrv {
// also the sub-delims "!", "'", "(", ")" and "*" are encoded;
// unicode handling uses UTF-8 as in ECMA-262.
encodeURIComponentStrict(str) {
- return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
- return '%' + c.charCodeAt(0).toString(16).toUpperCase();
+ return encodeURIComponent(str).replace(/[!'()*]/g, c => {
+ return (
+ '%' +
+ c
+ .charCodeAt(0)
+ .toString(16)
+ .toUpperCase()
+ );
});
}
@@ -256,11 +285,11 @@ export class TemplateSrv {
const value = this.grafanaVariables[variable.current.value];
- return typeof(value) === 'string' ? value : variable.current.text;
+ return typeof value === 'string' ? value : variable.current.text;
});
}
- fillVariableValuesForUrl(params, scopedVars) {
+ fillVariableValuesForUrl(params, scopedVars?) {
_.each(this.variables, variable => {
if (scopedVars && scopedVars[variable.name] !== void 0) {
if (scopedVars[variable.name].skipUrlSync) {
diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts
index 896987de706..b2f8b43fb08 100644
--- a/public/app/features/templating/variable_srv.ts
+++ b/public/app/features/templating/variable_srv.ts
@@ -6,23 +6,34 @@ import _ from 'lodash';
import coreModule from 'app/core/core_module';
import { variableTypes } from './variable';
import { Graph } from 'app/core/utils/dag';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
+import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
+
+// Types
+import { TimeRange } from '@grafana/ui/src';
export class VariableSrv {
- dashboard: any;
- variables: any;
+ dashboard: DashboardModel;
+ variables: any[];
/** @ngInject */
- constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) {
+ constructor(private $rootScope,
+ private $q,
+ private $location,
+ private $injector,
+ private templateSrv: TemplateSrv,
+ private timeSrv: TimeSrv) {
$rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope);
}
- init(dashboard) {
+ init(dashboard: DashboardModel) {
this.dashboard = dashboard;
this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this));
// create working class models representing variables
this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
- this.templateSrv.init(this.variables);
+ this.templateSrv.init(this.variables, this.timeSrv.timeRange());
// init variables
for (const variable of this.variables) {
@@ -37,11 +48,12 @@ export class VariableSrv {
})
)
.then(() => {
- this.templateSrv.updateTemplateData();
+ this.templateSrv.updateIndex();
});
}
- onTimeRangeUpdated() {
+ onTimeRangeUpdated(timeRange: TimeRange) {
+ this.templateSrv.updateTimeRange(timeRange);
const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => {
const previousOptions = variable.options.slice();
@@ -100,14 +112,14 @@ export class VariableSrv {
addVariable(variable) {
this.variables.push(variable);
- this.templateSrv.updateTemplateData();
+ this.templateSrv.updateIndex();
this.dashboard.updateSubmenuVisibility();
}
removeVariable(variable) {
const index = _.indexOf(this.variables, variable);
this.variables.splice(index, 1);
- this.templateSrv.updateTemplateData();
+ this.templateSrv.updateIndex();
this.dashboard.updateSubmenuVisibility();
}
diff --git a/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts b/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts
index 94e0e78d686..26c886043b2 100644
--- a/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts
+++ b/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts
@@ -22,6 +22,7 @@ const newVariable = index => {
};
export class ElasticPipelineVariablesCtrl {
+ /** @ngInject */
constructor($scope) {
$scope.variables = $scope.variables || [newVariable(1)];
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query_ctrl.ts
new file mode 100644
index 00000000000..294900205b7
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/query_ctrl.ts
@@ -0,0 +1,16 @@
+export class QueryCtrl {
+ target: any;
+ datasource: any;
+ panelCtrl: any;
+ panel: any;
+ hasRawMode: boolean;
+ error: string;
+
+ constructor(public $scope, _$injector) {
+ this.panelCtrl = this.panelCtrl || { panel: {} };
+ this.target = this.target || { target: '' };
+ this.panel = this.panelCtrl.panel;
+ }
+
+ refresh() {}
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/sdk.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/sdk.ts
new file mode 100644
index 00000000000..b8e8501ce71
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/__mocks__/sdk.ts
@@ -0,0 +1,3 @@
+import { QueryCtrl } from './query_ctrl';
+
+export { QueryCtrl };
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/annotations_query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/annotations_query_ctrl.ts
new file mode 100644
index 00000000000..8d9df1d13f3
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/annotations_query_ctrl.ts
@@ -0,0 +1,32 @@
+export class AzureMonitorAnnotationsQueryCtrl {
+ static templateUrl = 'partials/annotations.editor.html';
+ datasource: any;
+ annotation: any;
+ workspaces: any[];
+
+ defaultQuery = '\n| where $__timeFilter() \n| project TimeGenerated, Text=YourTitleColumn, Tags="tag1,tag2"';
+
+ /** @ngInject */
+ constructor() {
+ this.annotation.queryType = this.annotation.queryType || 'Azure Log Analytics';
+ this.annotation.rawQuery = this.annotation.rawQuery || this.defaultQuery;
+ this.getWorkspaces();
+ }
+
+ getWorkspaces() {
+ if (this.workspaces && this.workspaces.length > 0) {
+ return this.workspaces;
+ }
+
+ return this.datasource
+ .getAzureLogAnalyticsWorkspaces()
+ .then(list => {
+ this.workspaces = list;
+ if (list.length > 0 && !this.annotation.workspace) {
+ this.annotation.workspace = list[0].value;
+ }
+ return this.workspaces;
+ })
+ .catch(() => {});
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts
new file mode 100644
index 00000000000..35e15a5c885
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.test.ts
@@ -0,0 +1,441 @@
+import AzureMonitorDatasource from '../datasource';
+import Q from 'q';
+import moment from 'moment';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+describe('AppInsightsDatasource', () => {
+ const ctx: any = {
+ backendSrv: {},
+ templateSrv: new TemplateSrv(),
+ };
+
+ beforeEach(() => {
+ ctx.$q = Q;
+ ctx.instanceSettings = {
+ jsonData: { appInsightsAppId: '3ad4400f-ea7d-465d-a8fb-43fb20555d85' },
+ url: 'http://appinsightsapi',
+ };
+
+ ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
+ });
+
+ describe('When performing testDatasource', () => {
+ describe('and a list of metrics is returned', () => {
+ const response = {
+ metrics: {
+ 'requests/count': {
+ displayName: 'Server requests',
+ defaultAggregation: 'sum',
+ },
+ 'requests/duration': {
+ displayName: 'Server requests',
+ defaultAggregation: 'sum',
+ },
+ },
+ dimensions: {
+ 'request/source': {
+ displayName: 'Request source',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = () => {
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return success status', () => {
+ return ctx.ds.testDatasource().then(results => {
+ expect(results.status).toEqual('success');
+ });
+ });
+ });
+
+ describe('and a PathNotFoundError error is returned', () => {
+ const error = {
+ data: {
+ error: {
+ code: 'PathNotFoundError',
+ message: `An error message.`,
+ },
+ },
+ status: 404,
+ statusText: 'Not Found',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ return ctx.$q.reject(error);
+ };
+ });
+
+ it('should return error status and a detailed error message', () => {
+ return ctx.ds.testDatasource().then(results => {
+ expect(results.status).toEqual('error');
+ expect(results.message).toEqual(
+ '1. Application Insights: Not Found: Invalid Application Id for Application Insights service. '
+ );
+ });
+ });
+ });
+
+ describe('and an error is returned', () => {
+ const error = {
+ data: {
+ error: {
+ code: 'SomeOtherError',
+ message: `An error message.`,
+ },
+ },
+ status: 500,
+ statusText: 'Error',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ return ctx.$q.reject(error);
+ };
+ });
+
+ it('should return error status and a detailed error message', () => {
+ return ctx.ds.testDatasource().then(results => {
+ expect(results.status).toEqual('error');
+ expect(results.message).toEqual('1. Application Insights: Error: SomeOtherError. An error message. ');
+ });
+ });
+ });
+ });
+
+ describe('When performing query', () => {
+ const options = {
+ range: {
+ from: moment.utc('2017-08-22T20:00:00Z'),
+ to: moment.utc('2017-08-22T23:59:00Z'),
+ },
+ targets: [
+ {
+ apiVersion: '2016-09-01',
+ refId: 'A',
+ queryType: 'Application Insights',
+ appInsights: {
+ metricName: 'exceptions/server',
+ groupBy: '',
+ timeGrainType: 'none',
+ timeGrain: '',
+ timeGrainUnit: '',
+ alias: '',
+ },
+ },
+ ],
+ };
+
+ describe('and with a single value', () => {
+ const response = {
+ value: {
+ start: '2017-08-30T15:53:58.845Z',
+ end: '2017-09-06T15:53:58.845Z',
+ 'exceptions/server': {
+ sum: 100,
+ },
+ },
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/exceptions/server');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a single datapoint', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(1);
+ expect(results.data[0].datapoints.length).toBe(1);
+ expect(results.data[0].target).toEqual('exceptions/server');
+ expect(results.data[0].datapoints[0][1]).toEqual(1504713238845);
+ expect(results.data[0].datapoints[0][0]).toEqual(100);
+ });
+ });
+ });
+
+ describe('and with an interval group and without a segment group by', () => {
+ const response = {
+ value: {
+ start: '2017-08-30T15:53:58.845Z',
+ end: '2017-09-06T15:53:58.845Z',
+ interval: 'PT1H',
+ segments: [
+ {
+ start: '2017-08-30T15:53:58.845Z',
+ end: '2017-08-30T16:00:00.000Z',
+ 'exceptions/server': {
+ sum: 3,
+ },
+ },
+ {
+ start: '2017-08-30T16:00:00.000Z',
+ end: '2017-08-30T17:00:00.000Z',
+ 'exceptions/server': {
+ sum: 66,
+ },
+ },
+ ],
+ },
+ };
+
+ beforeEach(() => {
+ options.targets[0].appInsights.timeGrainType = 'specific';
+ options.targets[0].appInsights.timeGrain = '30';
+ options.targets[0].appInsights.timeGrainUnit = 'minute';
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/exceptions/server');
+ expect(options.url).toContain('interval=PT30M');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(1);
+ expect(results.data[0].datapoints.length).toBe(2);
+ expect(results.data[0].target).toEqual('exceptions/server');
+ expect(results.data[0].datapoints[0][1]).toEqual(1504108800000);
+ expect(results.data[0].datapoints[0][0]).toEqual(3);
+ expect(results.data[0].datapoints[1][1]).toEqual(1504112400000);
+ expect(results.data[0].datapoints[1][0]).toEqual(66);
+ });
+ });
+ });
+
+ describe('and with a group by', () => {
+ const response = {
+ value: {
+ start: '2017-08-30T15:53:58.845Z',
+ end: '2017-09-06T15:53:58.845Z',
+ interval: 'PT1H',
+ segments: [
+ {
+ start: '2017-08-30T15:53:58.845Z',
+ end: '2017-08-30T16:00:00.000Z',
+ segments: [
+ {
+ 'exceptions/server': {
+ sum: 10,
+ },
+ 'client/city': 'Miami',
+ },
+ {
+ 'exceptions/server': {
+ sum: 1,
+ },
+ 'client/city': 'San Jose',
+ },
+ ],
+ },
+ {
+ start: '2017-08-30T16:00:00.000Z',
+ end: '2017-08-30T17:00:00.000Z',
+ segments: [
+ {
+ 'exceptions/server': {
+ sum: 20,
+ },
+ 'client/city': 'Miami',
+ },
+ {
+ 'exceptions/server': {
+ sum: 2,
+ },
+ 'client/city': 'San Antonio',
+ },
+ ],
+ },
+ ],
+ },
+ };
+
+ describe('and with no alias specified', () => {
+ beforeEach(() => {
+ options.targets[0].appInsights.groupBy = 'client/city';
+
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/exceptions/server');
+ expect(options.url).toContain('segment=client/city');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(3);
+ expect(results.data[0].datapoints.length).toBe(2);
+ expect(results.data[0].target).toEqual('exceptions/server{client/city="Miami"}');
+ expect(results.data[0].datapoints[0][1]).toEqual(1504108800000);
+ expect(results.data[0].datapoints[0][0]).toEqual(10);
+ expect(results.data[0].datapoints[1][1]).toEqual(1504112400000);
+ expect(results.data[0].datapoints[1][0]).toEqual(20);
+ });
+ });
+ });
+
+ describe('and with an alias specified', () => {
+ beforeEach(() => {
+ options.targets[0].appInsights.groupBy = 'client/city';
+ options.targets[0].appInsights.alias = '{{metric}} + {{groupbyname}} + {{groupbyvalue}}';
+
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/exceptions/server');
+ expect(options.url).toContain('segment=client/city');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(3);
+ expect(results.data[0].datapoints.length).toBe(2);
+ expect(results.data[0].target).toEqual('exceptions/server + client/city + Miami');
+ expect(results.data[0].datapoints[0][1]).toEqual(1504108800000);
+ expect(results.data[0].datapoints[0][0]).toEqual(10);
+ expect(results.data[0].datapoints[1][1]).toEqual(1504112400000);
+ expect(results.data[0].datapoints[1][0]).toEqual(20);
+ });
+ });
+ });
+ });
+ });
+
+ describe('When performing metricFindQuery', () => {
+ describe('with a metric names query', () => {
+ const response = {
+ metrics: {
+ 'exceptions/server': {},
+ 'requests/count': {},
+ },
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/metadata');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of metric names', () => {
+ return ctx.ds.metricFindQuery('appInsightsMetricNames()').then(results => {
+ expect(results.length).toBe(2);
+ expect(results[0].text).toBe('exceptions/server');
+ expect(results[0].value).toBe('exceptions/server');
+ expect(results[1].text).toBe('requests/count');
+ expect(results[1].value).toBe('requests/count');
+ });
+ });
+ });
+
+ describe('with metadata group by query', () => {
+ const response = {
+ metrics: {
+ 'exceptions/server': {
+ supportedAggregations: ['sum'],
+ supportedGroupBy: {
+ all: ['client/os', 'client/city', 'client/browser'],
+ },
+ defaultAggregation: 'sum',
+ },
+ 'requests/count': {
+ supportedAggregations: ['avg', 'sum', 'total'],
+ supportedGroupBy: {
+ all: ['client/os', 'client/city', 'client/browser'],
+ },
+ defaultAggregation: 'avg',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/metadata');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of group bys', () => {
+ return ctx.ds.metricFindQuery('appInsightsGroupBys(requests/count)').then(results => {
+ expect(results[0].text).toContain('client/os');
+ expect(results[0].value).toContain('client/os');
+ expect(results[1].text).toContain('client/city');
+ expect(results[1].value).toContain('client/city');
+ expect(results[2].text).toContain('client/browser');
+ expect(results[2].value).toContain('client/browser');
+ });
+ });
+ });
+ });
+
+ describe('When getting Metric Names', () => {
+ const response = {
+ metrics: {
+ 'exceptions/server': {},
+ 'requests/count': {},
+ },
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/metadata');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of metric names', () => {
+ return ctx.ds.getAppInsightsMetricNames().then(results => {
+ expect(results.length).toBe(2);
+ expect(results[0].text).toBe('exceptions/server');
+ expect(results[0].value).toBe('exceptions/server');
+ expect(results[1].text).toBe('requests/count');
+ expect(results[1].value).toBe('requests/count');
+ });
+ });
+ });
+
+ describe('When getting Metric Metadata', () => {
+ const response = {
+ metrics: {
+ 'exceptions/server': {
+ supportedAggregations: ['sum'],
+ supportedGroupBy: {
+ all: ['client/os', 'client/city', 'client/browser'],
+ },
+ defaultAggregation: 'sum',
+ },
+ 'requests/count': {
+ supportedAggregations: ['avg', 'sum', 'total'],
+ supportedGroupBy: {
+ all: ['client/os', 'client/city', 'client/browser'],
+ },
+ defaultAggregation: 'avg',
+ },
+ },
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('/metrics/metadata');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of group bys', () => {
+ return ctx.ds.getAppInsightsMetricMetadata('requests/count').then(results => {
+ expect(results.primaryAggType).toEqual('avg');
+ expect(results.supportedAggTypes).toContain('avg');
+ expect(results.supportedAggTypes).toContain('sum');
+ expect(results.supportedAggTypes).toContain('total');
+ expect(results.supportedGroupBy).toContain('client/os');
+ expect(results.supportedGroupBy).toContain('client/city');
+ expect(results.supportedGroupBy).toContain('client/browser');
+ });
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts
new file mode 100644
index 00000000000..950fa73a16b
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts
@@ -0,0 +1,227 @@
+import _ from 'lodash';
+import AppInsightsQuerystringBuilder from './app_insights_querystring_builder';
+import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
+import ResponseParser from './response_parser';
+
+export interface LogAnalyticsColumn {
+ text: string;
+ value: string;
+}
+export default class AppInsightsDatasource {
+ id: number;
+ url: string;
+ baseUrl: string;
+ version = 'beta';
+ applicationId: string;
+ logAnalyticsColumns: { [key: string]: LogAnalyticsColumn[] } = {};
+
+ /** @ngInject */
+ constructor(instanceSettings, private backendSrv, private templateSrv, private $q) {
+ this.id = instanceSettings.id;
+ this.applicationId = instanceSettings.jsonData.appInsightsAppId;
+ this.baseUrl = `/appinsights/${this.version}/apps/${this.applicationId}`;
+ this.url = instanceSettings.url;
+ }
+
+ isConfigured(): boolean {
+ return !!this.applicationId && this.applicationId.length > 0;
+ }
+
+ query(options) {
+ const queries = _.filter(options.targets, item => {
+ return item.hide !== true;
+ }).map(target => {
+ const item = target.appInsights;
+ if (item.rawQuery) {
+ const querystringBuilder = new LogAnalyticsQuerystringBuilder(
+ this.templateSrv.replace(item.rawQueryString, options.scopedVars),
+ options,
+ 'timestamp'
+ );
+ const generated = querystringBuilder.generate();
+
+ const url = `${this.baseUrl}/query?${generated.uriString}`;
+
+ return {
+ refId: target.refId,
+ intervalMs: options.intervalMs,
+ maxDataPoints: options.maxDataPoints,
+ datasourceId: this.id,
+ url: url,
+ format: options.format,
+ alias: item.alias,
+ query: generated.rawQuery,
+ xaxis: item.xaxis,
+ yaxis: item.yaxis,
+ spliton: item.spliton,
+ raw: true,
+ };
+ } else {
+ const querystringBuilder = new AppInsightsQuerystringBuilder(
+ options.range.from,
+ options.range.to,
+ options.interval
+ );
+
+ if (item.groupBy !== 'none') {
+ querystringBuilder.setGroupBy(this.templateSrv.replace(item.groupBy, options.scopedVars));
+ }
+ querystringBuilder.setAggregation(item.aggregation);
+ querystringBuilder.setInterval(
+ item.timeGrainType,
+ this.templateSrv.replace(item.timeGrain, options.scopedVars),
+ item.timeGrainUnit
+ );
+
+ querystringBuilder.setFilter(this.templateSrv.replace(item.filter || ''));
+
+ const url = `${this.baseUrl}/metrics/${this.templateSrv.replace(
+ encodeURI(item.metricName),
+ options.scopedVars
+ )}?${querystringBuilder.generate()}`;
+
+ return {
+ refId: target.refId,
+ intervalMs: options.intervalMs,
+ maxDataPoints: options.maxDataPoints,
+ datasourceId: this.id,
+ url: url,
+ format: options.format,
+ alias: item.alias,
+ xaxis: '',
+ yaxis: '',
+ spliton: '',
+ raw: false,
+ };
+ }
+ });
+
+ if (!queries || queries.length === 0) {
+ return;
+ }
+
+ const promises = this.doQueries(queries);
+
+ return this.$q
+ .all(promises)
+ .then(results => {
+ return new ResponseParser(results).parseQueryResult();
+ })
+ .then(results => {
+ const flattened: any[] = [];
+
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].columnsForDropdown) {
+ this.logAnalyticsColumns[results[i].refId] = results[i].columnsForDropdown;
+ }
+ flattened.push(results[i]);
+ }
+
+ return flattened;
+ });
+ }
+
+ doQueries(queries) {
+ return _.map(queries, query => {
+ return this.doRequest(query.url)
+ .then(result => {
+ return {
+ result: result,
+ query: query,
+ };
+ })
+ .catch(err => {
+ throw {
+ error: err,
+ query: query,
+ };
+ });
+ });
+ }
+
+ annotationQuery(options) {}
+
+ metricFindQuery(query: string) {
+ const appInsightsMetricNameQuery = query.match(/^AppInsightsMetricNames\(\)/i);
+ if (appInsightsMetricNameQuery) {
+ return this.getMetricNames();
+ }
+
+ const appInsightsGroupByQuery = query.match(/^AppInsightsGroupBys\(([^\)]+?)(,\s?([^,]+?))?\)/i);
+ if (appInsightsGroupByQuery) {
+ const metricName = appInsightsGroupByQuery[1];
+ return this.getGroupBys(this.templateSrv.replace(metricName));
+ }
+
+ return undefined;
+ }
+
+ testDatasource() {
+ const url = `${this.baseUrl}/metrics/metadata`;
+ return this.doRequest(url)
+ .then(response => {
+ if (response.status === 200) {
+ return {
+ status: 'success',
+ message: 'Successfully queried the Application Insights service.',
+ title: 'Success',
+ };
+ }
+
+ return {
+ status: 'error',
+ message: 'Returned http status code ' + response.status,
+ };
+ })
+ .catch(error => {
+ let message = 'Application Insights: ';
+ message += error.statusText ? error.statusText + ': ' : '';
+
+ if (error.data && error.data.error && error.data.error.code === 'PathNotFoundError') {
+ message += 'Invalid Application Id for Application Insights service.';
+ } else if (error.data && error.data.error) {
+ message += error.data.error.code + '. ' + error.data.error.message;
+ } else {
+ message += 'Cannot connect to Application Insights REST API.';
+ }
+
+ return {
+ status: 'error',
+ message: message,
+ };
+ });
+ }
+
+ doRequest(url, maxRetries = 1) {
+ return this.backendSrv
+ .datasourceRequest({
+ url: this.url + url,
+ method: 'GET',
+ })
+ .catch(error => {
+ if (maxRetries > 0) {
+ return this.doRequest(url, maxRetries - 1);
+ }
+
+ throw error;
+ });
+ }
+
+ getMetricNames() {
+ const url = `${this.baseUrl}/metrics/metadata`;
+ return this.doRequest(url).then(ResponseParser.parseMetricNames);
+ }
+
+ getMetricMetadata(metricName: string) {
+ const url = `${this.baseUrl}/metrics/metadata`;
+ return this.doRequest(url).then(result => {
+ return new ResponseParser(result).parseMetadata(metricName);
+ });
+ }
+
+ getGroupBys(metricName: string) {
+ return this.getMetricMetadata(metricName).then(result => {
+ return new ResponseParser(result).parseGroupBys();
+ });
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.test.ts
new file mode 100644
index 00000000000..a38ec95dc50
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.test.ts
@@ -0,0 +1,72 @@
+import AppInsightsQuerystringBuilder from './app_insights_querystring_builder';
+import moment from 'moment';
+
+describe('AppInsightsQuerystringBuilder', () => {
+ let builder: AppInsightsQuerystringBuilder;
+
+ beforeEach(() => {
+ builder = new AppInsightsQuerystringBuilder(moment.utc('2017-08-22 06:00'), moment.utc('2017-08-22 07:00'), '1h');
+ });
+
+ describe('with only from/to date range', () => {
+ it('should always add datetime filtering to the querystring', () => {
+ const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z`;
+ expect(builder.generate()).toEqual(querystring);
+ });
+ });
+
+ describe('with from/to date range and aggregation type', () => {
+ beforeEach(() => {
+ builder.setAggregation('avg');
+ });
+
+ it('should add datetime filtering and aggregation to the querystring', () => {
+ const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&aggregation=avg`;
+ expect(builder.generate()).toEqual(querystring);
+ });
+ });
+
+ describe('with from/to date range and group by segment', () => {
+ beforeEach(() => {
+ builder.setGroupBy('client/city');
+ });
+
+ it('should add datetime filtering and segment to the querystring', () => {
+ const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&segment=client/city`;
+ expect(builder.generate()).toEqual(querystring);
+ });
+ });
+
+ describe('with from/to date range and specific group by interval', () => {
+ beforeEach(() => {
+ builder.setInterval('specific', 1, 'hour');
+ });
+
+ it('should add datetime filtering and interval to the querystring', () => {
+ const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&interval=PT1H`;
+ expect(builder.generate()).toEqual(querystring);
+ });
+ });
+
+ describe('with from/to date range and auto group by interval', () => {
+ beforeEach(() => {
+ builder.setInterval('auto', '', '');
+ });
+
+ it('should add datetime filtering and interval to the querystring', () => {
+ const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&interval=PT1H`;
+ expect(builder.generate()).toEqual(querystring);
+ });
+ });
+
+ describe('with filter', () => {
+ beforeEach(() => {
+ builder.setFilter(`client/city eq 'Boydton'`);
+ });
+
+ it('should add datetime filtering and interval to the querystring', () => {
+ const querystring = `timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z&filter=client/city eq 'Boydton'`;
+ expect(builder.generate()).toEqual(querystring);
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.ts
new file mode 100644
index 00000000000..9b97a1b5e31
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_querystring_builder.ts
@@ -0,0 +1,56 @@
+import TimeGrainConverter from '../time_grain_converter';
+
+export default class AppInsightsQuerystringBuilder {
+ aggregation = '';
+ groupBy = '';
+ timeGrainType = '';
+ timeGrain = '';
+ timeGrainUnit = '';
+ filter = '';
+
+ constructor(private from, private to, public grafanaInterval) {}
+
+ setAggregation(aggregation) {
+ this.aggregation = aggregation;
+ }
+
+ setGroupBy(groupBy) {
+ this.groupBy = groupBy;
+ }
+
+ setInterval(timeGrainType, timeGrain, timeGrainUnit) {
+ this.timeGrainType = timeGrainType;
+ this.timeGrain = timeGrain;
+ this.timeGrainUnit = timeGrainUnit;
+ }
+
+ setFilter(filter: string) {
+ this.filter = filter;
+ }
+
+ generate() {
+ let querystring = `timespan=${this.from.utc().format()}/${this.to.utc().format()}`;
+
+ if (this.aggregation && this.aggregation.length > 0) {
+ querystring += `&aggregation=${this.aggregation}`;
+ }
+
+ if (this.groupBy && this.groupBy.length > 0) {
+ querystring += `&segment=${this.groupBy}`;
+ }
+
+ if (this.timeGrainType === 'specific' && this.timeGrain && this.timeGrainUnit) {
+ querystring += `&interval=${TimeGrainConverter.createISO8601Duration(this.timeGrain, this.timeGrainUnit)}`;
+ }
+
+ if (this.timeGrainType === 'auto') {
+ querystring += `&interval=${TimeGrainConverter.createISO8601DurationFromInterval(this.grafanaInterval)}`;
+ }
+
+ if (this.filter) {
+ querystring += `&filter=${this.filter}`;
+ }
+
+ return querystring;
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts
new file mode 100644
index 00000000000..848472cf101
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts
@@ -0,0 +1,212 @@
+import moment from 'moment';
+import _ from 'lodash';
+
+export default class ResponseParser {
+ constructor(private results) {}
+
+ parseQueryResult() {
+ let data: any = [];
+ let columns: any = [];
+ for (let i = 0; i < this.results.length; i++) {
+ if (this.results[i].query.raw) {
+ const xaxis = this.results[i].query.xaxis;
+ const yaxises = this.results[i].query.yaxis;
+ const spliton = this.results[i].query.spliton;
+ columns = this.results[i].result.data.Tables[0].Columns;
+ const rows = this.results[i].result.data.Tables[0].Rows;
+ data = _.concat(
+ data,
+ this.parseRawQueryResultRow(this.results[i].query, columns, rows, xaxis, yaxises, spliton)
+ );
+ } else {
+ const value = this.results[i].result.data.value;
+ const alias = this.results[i].query.alias;
+ data = _.concat(data, this.parseQueryResultRow(this.results[i].query, value, alias));
+ }
+ }
+ return data;
+ }
+
+ parseRawQueryResultRow(query: any, columns, rows, xaxis: string, yaxises: string, spliton: string) {
+ const data: any[] = [];
+ const columnsForDropdown = _.map(columns, column => ({ text: column.ColumnName, value: column.ColumnName }));
+
+ const xaxisColumn = columns.findIndex(column => column.ColumnName === xaxis);
+ const yaxisesSplit = yaxises.split(',');
+ const yaxisColumns = {};
+ _.forEach(yaxisesSplit, yaxis => {
+ yaxisColumns[yaxis] = columns.findIndex(column => column.ColumnName === yaxis);
+ });
+ const splitonColumn = columns.findIndex(column => column.ColumnName === spliton);
+ const convertTimestamp = xaxis === 'timestamp';
+
+ _.forEach(rows, row => {
+ _.forEach(yaxisColumns, (yaxisColumn, yaxisName) => {
+ const bucket =
+ splitonColumn === -1
+ ? ResponseParser.findOrCreateBucket(data, yaxisName)
+ : ResponseParser.findOrCreateBucket(data, row[splitonColumn]);
+ const epoch = convertTimestamp ? ResponseParser.dateTimeToEpoch(row[xaxisColumn]) : row[xaxisColumn];
+ bucket.datapoints.push([row[yaxisColumn], epoch]);
+ bucket.refId = query.refId;
+ bucket.query = query.query;
+ bucket.columnsForDropdown = columnsForDropdown;
+ });
+ });
+
+ return data;
+ }
+
+ parseQueryResultRow(query: any, value, alias: string) {
+ const data: any[] = [];
+
+ if (ResponseParser.isSingleValue(value)) {
+ const metricName = ResponseParser.getMetricFieldKey(value);
+ const aggField = ResponseParser.getKeyForAggregationField(value[metricName]);
+ const epoch = ResponseParser.dateTimeToEpoch(value.end);
+ data.push({
+ target: metricName,
+ datapoints: [[value[metricName][aggField], epoch]],
+ refId: query.refId,
+ query: query.query,
+ });
+ return data;
+ }
+
+ const groupedBy = ResponseParser.hasSegmentsField(value.segments[0]);
+ if (!groupedBy) {
+ const metricName = ResponseParser.getMetricFieldKey(value.segments[0]);
+ const dataTarget = ResponseParser.findOrCreateBucket(data, metricName);
+
+ for (let i = 0; i < value.segments.length; i++) {
+ const epoch = ResponseParser.dateTimeToEpoch(value.segments[i].end);
+ const aggField: string = ResponseParser.getKeyForAggregationField(value.segments[i][metricName]);
+
+ dataTarget.datapoints.push([value.segments[i][metricName][aggField], epoch]);
+ }
+ dataTarget.refId = query.refId;
+ dataTarget.query = query.query;
+ } else {
+ for (let i = 0; i < value.segments.length; i++) {
+ const epoch = ResponseParser.dateTimeToEpoch(value.segments[i].end);
+
+ for (let j = 0; j < value.segments[i].segments.length; j++) {
+ const metricName = ResponseParser.getMetricFieldKey(value.segments[i].segments[j]);
+ const aggField = ResponseParser.getKeyForAggregationField(value.segments[i].segments[j][metricName]);
+ const target = this.getTargetName(value.segments[i].segments[j], alias);
+
+ const bucket = ResponseParser.findOrCreateBucket(data, target);
+ bucket.datapoints.push([value.segments[i].segments[j][metricName][aggField], epoch]);
+ bucket.refId = query.refId;
+ bucket.query = query.query;
+ }
+ }
+ }
+
+ return data;
+ }
+
+ getTargetName(segment, alias: string) {
+ let metric = '';
+ let segmentName = '';
+ let segmentValue = '';
+ for (const prop in segment) {
+ if (_.isObject(segment[prop])) {
+ metric = prop;
+ } else {
+ segmentName = prop;
+ segmentValue = segment[prop];
+ }
+ }
+
+ if (alias) {
+ const regex = /\{\{([\s\S]+?)\}\}/g;
+ return alias.replace(regex, (match, g1, g2) => {
+ const group = g1 || g2;
+
+ if (group === 'metric') {
+ return metric;
+ } else if (group === 'groupbyname') {
+ return segmentName;
+ } else if (group === 'groupbyvalue') {
+ return segmentValue;
+ }
+
+ return match;
+ });
+ }
+
+ return metric + `{${segmentName}="${segmentValue}"}`;
+ }
+
+ static isSingleValue(value) {
+ return !ResponseParser.hasSegmentsField(value);
+ }
+
+ static findOrCreateBucket(data, target) {
+ let dataTarget = _.find(data, ['target', target]);
+ if (!dataTarget) {
+ dataTarget = { target: target, datapoints: [] };
+ data.push(dataTarget);
+ }
+
+ return dataTarget;
+ }
+
+ static hasSegmentsField(obj) {
+ const keys = _.keys(obj);
+ return _.indexOf(keys, 'segments') > -1;
+ }
+
+ static getMetricFieldKey(segment) {
+ const keys = _.keys(segment);
+
+ return _.filter(_.without(keys, 'start', 'end'), key => {
+ return _.isObject(segment[key]);
+ })[0];
+ }
+
+ static getKeyForAggregationField(dataObj): string {
+ const keys = _.keys(dataObj);
+ return _.intersection(keys, ['sum', 'avg', 'min', 'max', 'count', 'unique'])[0];
+ }
+
+ static dateTimeToEpoch(dateTime) {
+ return moment(dateTime).valueOf();
+ }
+
+ static parseMetricNames(result) {
+ const keys = _.keys(result.data.metrics);
+
+ return ResponseParser.toTextValueList(keys);
+ }
+
+ parseMetadata(metricName: string) {
+ const metric = this.results.data.metrics[metricName];
+
+ if (!metric) {
+ throw Error('No data found for metric: ' + metricName);
+ }
+
+ return {
+ primaryAggType: metric.defaultAggregation,
+ supportedAggTypes: metric.supportedAggregations,
+ supportedGroupBy: metric.supportedGroupBy.all,
+ };
+ }
+
+ parseGroupBys() {
+ return ResponseParser.toTextValueList(this.results.supportedGroupBy);
+ }
+
+ static toTextValueList(values) {
+ const list: any[] = [];
+ for (let i = 0; i < values.length; i++) {
+ list.push({
+ text: values[i],
+ value: values[i],
+ });
+ }
+ return list;
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/__mocks__/schema.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/__mocks__/schema.ts
new file mode 100644
index 00000000000..01b77a894ad
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/__mocks__/schema.ts
@@ -0,0 +1,305 @@
+export default class FakeSchemaData {
+ static getLogAnalyticsFakeSchema() {
+ return {
+ Tables: [
+ {
+ TableName: 'Table_0',
+ Columns: [
+ {
+ ColumnName: 'TableName',
+ DataType: 'String',
+ },
+ {
+ ColumnName: 'ColumnName',
+ DataType: 'String',
+ },
+ {
+ ColumnName: 'ColumnType',
+ DataType: 'String',
+ },
+ ],
+ Rows: [
+ ['AzureNetworkAnalytics_CL', 'SourceSystem', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'ManagementGroupName', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'TimeGenerated', 'System.DateTime'],
+ ['AzureNetworkAnalytics_CL', 'Computer', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'FASchemaVersion_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'FlowType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SrcIP_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'DestIP_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VMIP_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'L4Protocol_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'L7Protocol_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'FlowDirection_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'NSGList_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'NSGRules_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'HopNSGList_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'HopNSGRules_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Region1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Region2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'NIC_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'NIC1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'NIC2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VM_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VM1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VM2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Subnet_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'ConnectionName_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'S2SConnection_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'S2SConnectionType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Country_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'AzureRegion_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Subscription1_g', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Subscription2_g', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'FlowStartTime_t', 'System.DateTime'],
+ ['AzureNetworkAnalytics_CL', 'FlowEndTime_t', 'System.DateTime'],
+ ['AzureNetworkAnalytics_CL', 'DestPort_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'AllowedInFlows_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'DeniedInFlows_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'AllowedOutFlows_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'DeniedOutFlows_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'DeniedInFlowsAtHops_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'DeniedOutFlowsAtHops_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'FlowCount_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'NextHopIP_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'IsVirtualAppliance_b', 'System.Boolean'],
+ ['AzureNetworkAnalytics_CL', 'AddressPrefix_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'NextHopType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'RouteTable_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Subnet1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Subnet2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SubnetRegion1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SubnetRegion2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualAppliances_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'AllowForwardedTraffic_b', 'System.Boolean'],
+ ['AzureNetworkAnalytics_CL', 'AllowGatewayTransit_b', 'System.Boolean'],
+ ['AzureNetworkAnalytics_CL', 'AllowVirtualNetworkAccess_b', 'System.Boolean'],
+ ['AzureNetworkAnalytics_CL', 'UseRemoteGateways_b', 'System.Boolean'],
+ ['AzureNetworkAnalytics_CL', 'NSG_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'PrivateIPAddresses_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'PublicIPAddresses_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Subnetwork_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualMachine_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'MACAddress_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'AddressPrefixes_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'ConnectingVirtualNetwork_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'RemoteVirtualNetworkGateway_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'IsFlowEnabled_b', 'System.Boolean'],
+ ['AzureNetworkAnalytics_CL', 'GatewayType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SKU_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VIPAddress_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualSubnetwork_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'BGPEnabled_b', 'System.Boolean'],
+ ['AzureNetworkAnalytics_CL', 'ConnectionStatus_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'ConnectionType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'GatewayConnectionType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'LocalNetworkGateway_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualNetwork1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualNetwork2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualNetworkGateway1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualNetworkGateway2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualNetworkRegion1_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'VirtualNetworkRegion2_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'EgressBytesTransferred_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'IngressBytesTransferred_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'RoutingWeight_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'FrontendSubnet_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'LoadBalancerType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Access_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Description_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'DestinationAddressPrefix_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'DestinationPortRange_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Direction_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Protocol_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'RuleType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SourceAddressPrefix_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SourcePortRange_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Priority_d', 'System.Double'],
+ ['AzureNetworkAnalytics_CL', 'IPAddress', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SubnetPrefixes_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SchemaVersion_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Name_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Region_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'AppGatewayType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'BackendSubnets_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'FrontendIPs_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'GatewaySubnet_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'ComponentType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'DiscoveryRegion_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'ResourceType', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Status_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'SubType_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'TopologyVersion_s', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'Subscription_g', 'System.String'],
+ ['AzureNetworkAnalytics_CL', 'TimeProcessed_t', 'System.DateTime'],
+ ['AzureNetworkAnalytics_CL', 'Type', 'System.String'],
+ ],
+ KqlPrimaryTimestampColumnName: 'TimeGenerated',
+ },
+ {
+ TableName: 'Table_1',
+ Columns: [
+ {
+ ColumnName: 'TableType',
+ DataType: 'String',
+ },
+ {
+ ColumnName: 'TableName',
+ DataType: 'String',
+ },
+ {
+ ColumnName: 'PrimaryTimestampColumnName',
+ DataType: 'String',
+ },
+ {
+ ColumnName: 'Solutions',
+ DataType: 'String',
+ },
+ ],
+ Rows: [['oms', 'AzureNetworkAnalytics_CL', 'TimeGenerated', 'LogManagement']],
+ },
+ ],
+ };
+ }
+
+ static getlogAnalyticsFakeMetadata() {
+ return {
+ tables: [
+ {
+ id: 't/Alert',
+ name: 'Alert',
+ timespanColumn: 'TimeGenerated',
+ columns: [
+ { name: 'TimeGenerated', type: 'datetime' },
+ { name: 'AlertSeverity', type: 'string' },
+ { name: 'SourceDisplayName', type: 'string' },
+ { name: 'AlertName', type: 'string' },
+ { name: 'AlertDescription', type: 'string' },
+ { name: 'SourceSystem', type: 'string' },
+ { name: 'QueryExecutionStartTime', type: 'datetime' },
+ { name: 'QueryExecutionEndTime', type: 'datetime' },
+ { name: 'Query', type: 'string' },
+ { name: 'RemediationJobId', type: 'string' },
+ { name: 'RemediationRunbookName', type: 'string' },
+ { name: 'AlertRuleId', type: 'string' },
+ { name: 'AlertRuleInstanceId', type: 'string' },
+ { name: 'ThresholdOperator', type: 'string' },
+ { name: 'ThresholdValue', type: 'int' },
+ { name: 'LinkToSearchResults', type: 'string' },
+ { name: 'ServiceDeskConnectionName', type: 'string' },
+ { name: 'ServiceDeskId', type: 'string' },
+ { name: 'ServiceDeskWorkItemLink', type: 'string' },
+ { name: 'ServiceDeskWorkItemType', type: 'string' },
+ { name: 'ResourceId', type: 'string' },
+ { name: 'ResourceType', type: 'string' },
+ { name: 'ResourceValue', type: 'string' },
+ { name: 'RootObjectName', type: 'string' },
+ { name: 'ObjectDisplayName', type: 'string' },
+ { name: 'Computer', type: 'string' },
+ { name: 'AlertPriority', type: 'string' },
+ { name: 'SourceFullName', type: 'string' },
+ { name: 'AlertId', type: 'string' },
+ { name: 'RepeatCount', type: 'int' },
+ { name: 'AlertState', type: 'string' },
+ { name: 'ResolvedBy', type: 'string' },
+ { name: 'LastModifiedBy', type: 'string' },
+ { name: 'TimeRaised', type: 'datetime' },
+ { name: 'TimeResolved', type: 'datetime' },
+ { name: 'TimeLastModified', type: 'datetime' },
+ { name: 'AlertContext', type: 'string' },
+ { name: 'TicketId', type: 'string' },
+ { name: 'Custom1', type: 'string' },
+ { name: 'Custom2', type: 'string' },
+ { name: 'Custom3', type: 'string' },
+ { name: 'Custom4', type: 'string' },
+ { name: 'Custom5', type: 'string' },
+ { name: 'Custom6', type: 'string' },
+ { name: 'Custom7', type: 'string' },
+ { name: 'Custom8', type: 'string' },
+ { name: 'Custom9', type: 'string' },
+ { name: 'Custom10', type: 'string' },
+ { name: 'ManagementGroupName', type: 'string' },
+ { name: 'PriorityNumber', type: 'int' },
+ { name: 'HostName', type: 'string' },
+ { name: 'StateType', type: 'string' },
+ { name: 'AlertTypeDescription', type: 'string' },
+ { name: 'AlertTypeNumber', type: 'int' },
+ { name: 'AlertError', type: 'string' },
+ { name: 'StatusDescription', type: 'string' },
+ { name: 'AlertStatus', type: 'int' },
+ { name: 'TriggerId', type: 'string' },
+ { name: 'Url', type: 'string' },
+ { name: 'ValueDescription', type: 'string' },
+ { name: 'AlertValue', type: 'int' },
+ { name: 'Comments', type: 'string' },
+ { name: 'TemplateId', type: 'string' },
+ { name: 'FlagsDescription', type: 'string' },
+ { name: 'Flags', type: 'int' },
+ { name: 'ValueFlagsDescription', type: 'string' },
+ { name: 'ValueFlags', type: 'int' },
+ { name: 'Expression', type: 'string' },
+ { name: 'Type', type: 'string' },
+ ],
+ },
+ {
+ id: 't/AzureActivity',
+ name: 'AzureActivity',
+ timespanColumn: 'TimeGenerated',
+ columns: [
+ { name: 'OperationName', type: 'string' },
+ { name: 'Level', type: 'string' },
+ { name: 'ActivityStatus', type: 'string' },
+ { name: 'ActivitySubstatus', type: 'string' },
+ { name: 'ResourceGroup', type: 'string' },
+ { name: 'SubscriptionId', type: 'string' },
+ { name: 'CorrelationId', type: 'string' },
+ { name: 'Caller', type: 'string' },
+ { name: 'CallerIpAddress', type: 'string' },
+ { name: 'Category', type: 'string' },
+ { name: 'HTTPRequest', type: 'string' },
+ { name: 'Properties', type: 'string' },
+ { name: 'EventSubmissionTimestamp', type: 'datetime' },
+ { name: 'Authorization', type: 'string' },
+ { name: 'ResourceId', type: 'string' },
+ { name: 'OperationId', type: 'string' },
+ { name: 'ResourceProvider', type: 'string' },
+ { name: 'Resource', type: 'string' },
+ { name: 'TimeGenerated', type: 'datetime' },
+ { name: 'SourceSystem', type: 'string' },
+ { name: 'Type', type: 'string' },
+ ],
+ },
+ ],
+ tableGroups: [
+ {
+ id: 'oms/LogManagement',
+ name: 'LogManagement',
+ source: 'oms',
+ tables: ['t/Alert', 't/AzureActivity'],
+ },
+ ],
+ functions: [
+ {
+ id: 'f/Func1',
+ name: 'Func1',
+ displayName: 'Func1',
+ body: 'AzureActivity\n| where ActivityStatus == "" \n',
+ category: 'test',
+ },
+ ],
+ applications: [],
+ workspaces: [
+ {
+ id: 'a2c1b44e-3e57-4410-b027-999999999999',
+ name: 'danieltest',
+ resourceId:
+ '/subscriptions/44693801-6ee6-49de-9b2d-999999999999/resourceGroups/danieltest/providers/' +
+ 'microsoft.operationalinsights/workspaces/danieltest',
+ tables: [],
+ tableGroups: ['oms/LogManagement'],
+ functions: ['f/Func1'],
+ },
+ ],
+ };
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts
new file mode 100644
index 00000000000..199a63583d2
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.test.ts
@@ -0,0 +1,401 @@
+import AzureMonitorDatasource from '../datasource';
+import FakeSchemaData from './__mocks__/schema';
+import Q from 'q';
+import moment from 'moment';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+describe('AzureLogAnalyticsDatasource', () => {
+ const ctx: any = {
+ backendSrv: {},
+ templateSrv: new TemplateSrv(),
+ };
+
+ beforeEach(() => {
+ ctx.$q = Q;
+ ctx.instanceSettings = {
+ jsonData: { logAnalyticsSubscriptionId: 'xxx' },
+ url: 'http://azureloganalyticsapi',
+ };
+
+ ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
+ });
+
+ describe('When the config option "Same as Azure Monitor" has been chosen', () => {
+ const tableResponseWithOneColumn = {
+ tables: [
+ {
+ name: 'PrimaryResult',
+ columns: [
+ {
+ name: 'Category',
+ type: 'string',
+ },
+ ],
+ rows: [['Administrative'], ['Policy']],
+ },
+ ],
+ };
+
+ const workspaceResponse = {
+ value: [
+ {
+ name: 'aworkspace',
+ properties: {
+ source: 'Azure',
+ customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
+ },
+ },
+ ],
+ };
+
+ let workspacesUrl;
+ let azureLogAnalyticsUrl;
+
+ beforeEach(async () => {
+ ctx.instanceSettings.jsonData.subscriptionId = 'xxx';
+ ctx.instanceSettings.jsonData.tenantId = 'xxx';
+ ctx.instanceSettings.jsonData.clientId = 'xxx';
+ ctx.instanceSettings.jsonData.azureLogAnalyticsSameAs = true;
+ ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
+
+ ctx.backendSrv.datasourceRequest = options => {
+ if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
+ workspacesUrl = options.url;
+ return ctx.$q.when({ data: workspaceResponse, status: 200 });
+ } else {
+ azureLogAnalyticsUrl = options.url;
+ return ctx.$q.when({ data: tableResponseWithOneColumn, status: 200 });
+ }
+ };
+
+ await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
+ });
+
+ it('should use the sameasloganalyticsazure plugin route', () => {
+ expect(workspacesUrl).toContain('azuremonitor');
+ expect(azureLogAnalyticsUrl).toContain('sameasloganalyticsazure');
+ });
+ });
+
+ describe('When performing testDatasource', () => {
+ describe('and an error is returned', () => {
+ const error = {
+ data: {
+ error: {
+ code: 'InvalidApiVersionParameter',
+ message: `An error message.`,
+ },
+ },
+ status: 400,
+ statusText: 'Bad Request',
+ };
+
+ beforeEach(() => {
+ ctx.instanceSettings.jsonData.logAnalyticsSubscriptionId = 'xxx';
+ ctx.instanceSettings.jsonData.logAnalyticsTenantId = 'xxx';
+ ctx.instanceSettings.jsonData.logAnalyticsClientId = 'xxx';
+ ctx.backendSrv.datasourceRequest = () => {
+ return ctx.$q.reject(error);
+ };
+ });
+
+ it('should return error status and a detailed error message', () => {
+ return ctx.ds.testDatasource().then(results => {
+ expect(results.status).toEqual('error');
+ expect(results.message).toEqual(
+ '1. Azure Log Analytics: Bad Request: InvalidApiVersionParameter. An error message. '
+ );
+ });
+ });
+ });
+ });
+
+ describe('When performing query', () => {
+ const options = {
+ range: {
+ from: moment.utc('2017-08-22T20:00:00Z'),
+ to: moment.utc('2017-08-22T23:59:00Z'),
+ },
+ rangeRaw: {
+ from: 'now-4h',
+ to: 'now',
+ },
+ targets: [
+ {
+ apiVersion: '2016-09-01',
+ refId: 'A',
+ queryType: 'Azure Log Analytics',
+ azureLogAnalytics: {
+ resultFormat: 'time_series',
+ query:
+ 'AzureActivity | where TimeGenerated > ago(2h) ' +
+ '| summarize count() by Category, bin(TimeGenerated, 5min) ' +
+ '| project TimeGenerated, Category, count_ | order by TimeGenerated asc',
+ },
+ },
+ ],
+ };
+
+ const response = {
+ tables: [
+ {
+ name: 'PrimaryResult',
+ columns: [
+ {
+ name: 'TimeGenerated',
+ type: 'datetime',
+ },
+ {
+ name: 'Category',
+ type: 'string',
+ },
+ {
+ name: 'count_',
+ type: 'long',
+ },
+ ],
+ rows: [
+ ['2018-06-02T20:20:00Z', 'Administrative', 2],
+ ['2018-06-02T20:25:00Z', 'Administrative', 22],
+ ['2018-06-02T20:30:00Z', 'Policy', 20],
+ ],
+ },
+ ],
+ };
+
+ describe('in time series format', () => {
+ describe('and the data is valid (has time, metric and value columns)', () => {
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('query=AzureActivity');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(2);
+ expect(results.data[0].datapoints.length).toBe(2);
+ expect(results.data[0].target).toEqual('Administrative');
+ expect(results.data[0].datapoints[0][1]).toEqual(1527970800000);
+ expect(results.data[0].datapoints[0][0]).toEqual(2);
+ expect(results.data[0].datapoints[1][1]).toEqual(1527971100000);
+ expect(results.data[0].datapoints[1][0]).toEqual(22);
+ });
+ });
+ });
+
+ describe('and the data has no time column)', () => {
+ beforeEach(() => {
+ const invalidResponse = {
+ tables: [
+ {
+ name: 'PrimaryResult',
+ columns: [
+ {
+ name: 'Category',
+ type: 'string',
+ },
+ {
+ name: 'count_',
+ type: 'long',
+ },
+ ],
+ rows: [['Administrative', 2]],
+ },
+ ],
+ };
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('query=AzureActivity');
+ return ctx.$q.when({ data: invalidResponse, status: 200 });
+ };
+ });
+
+ it('should throw an exception', () => {
+ ctx.ds.query(options).catch(err => {
+ expect(err.message).toContain('The Time Series format requires a time column.');
+ });
+ });
+ });
+ });
+
+ describe('in tableformat', () => {
+ beforeEach(() => {
+ options.targets[0].azureLogAnalytics.resultFormat = 'table';
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('query=AzureActivity');
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of columns and rows', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data[0].type).toBe('table');
+ expect(results.data[0].columns.length).toBe(3);
+ expect(results.data[0].rows.length).toBe(3);
+ expect(results.data[0].columns[0].text).toBe('TimeGenerated');
+ expect(results.data[0].columns[0].type).toBe('datetime');
+ expect(results.data[0].columns[1].text).toBe('Category');
+ expect(results.data[0].columns[1].type).toBe('string');
+ expect(results.data[0].columns[2].text).toBe('count_');
+ expect(results.data[0].columns[2].type).toBe('long');
+ expect(results.data[0].rows[0][0]).toEqual('2018-06-02T20:20:00Z');
+ expect(results.data[0].rows[0][1]).toEqual('Administrative');
+ expect(results.data[0].rows[0][2]).toEqual(2);
+ });
+ });
+ });
+ });
+
+ describe('When performing getSchema', () => {
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain('metadata');
+ return ctx.$q.when({ data: FakeSchemaData.getlogAnalyticsFakeMetadata(), status: 200 });
+ };
+ });
+
+ it('should return a schema with a table and rows', () => {
+ return ctx.ds.azureLogAnalyticsDatasource.getSchema('myWorkspace').then(result => {
+ expect(Object.keys(result.Databases.Default.Tables).length).toBe(2);
+ expect(result.Databases.Default.Tables.Alert.Name).toBe('Alert');
+ expect(result.Databases.Default.Tables.AzureActivity.Name).toBe('AzureActivity');
+ expect(result.Databases.Default.Tables.Alert.OrderedColumns.length).toBe(69);
+ expect(result.Databases.Default.Tables.AzureActivity.OrderedColumns.length).toBe(21);
+ expect(result.Databases.Default.Tables.Alert.OrderedColumns[0].Name).toBe('TimeGenerated');
+ expect(result.Databases.Default.Tables.Alert.OrderedColumns[0].Type).toBe('datetime');
+
+ expect(Object.keys(result.Databases.Default.Functions).length).toBe(1);
+ expect(result.Databases.Default.Functions.Func1.Name).toBe('Func1');
+ });
+ });
+ });
+
+ describe('When performing metricFindQuery', () => {
+ const tableResponseWithOneColumn = {
+ tables: [
+ {
+ name: 'PrimaryResult',
+ columns: [
+ {
+ name: 'Category',
+ type: 'string',
+ },
+ ],
+ rows: [['Administrative'], ['Policy']],
+ },
+ ],
+ };
+
+ const workspaceResponse = {
+ value: [
+ {
+ name: 'aworkspace',
+ properties: {
+ source: 'Azure',
+ customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
+ },
+ },
+ ],
+ };
+
+ let queryResults;
+
+ beforeEach(async () => {
+ ctx.backendSrv.datasourceRequest = options => {
+ if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
+ return ctx.$q.when({ data: workspaceResponse, status: 200 });
+ } else {
+ return ctx.$q.when({ data: tableResponseWithOneColumn, status: 200 });
+ }
+ };
+
+ queryResults = await ctx.ds.metricFindQuery('workspace("aworkspace").AzureActivity | distinct Category');
+ });
+
+ it('should return a list of categories in the correct format', () => {
+ expect(queryResults.length).toBe(2);
+ expect(queryResults[0].text).toBe('Administrative');
+ expect(queryResults[0].value).toBe('Administrative');
+ expect(queryResults[1].text).toBe('Policy');
+ expect(queryResults[1].value).toBe('Policy');
+ });
+ });
+
+ describe('When performing annotationQuery', () => {
+ const tableResponse = {
+ tables: [
+ {
+ name: 'PrimaryResult',
+ columns: [
+ {
+ name: 'TimeGenerated',
+ type: 'datetime',
+ },
+ {
+ name: 'Text',
+ type: 'string',
+ },
+ {
+ name: 'Tags',
+ type: 'string',
+ },
+ ],
+ rows: [['2018-06-02T20:20:00Z', 'Computer1', 'tag1,tag2'], ['2018-06-02T20:28:00Z', 'Computer2', 'tag2']],
+ },
+ ],
+ };
+
+ const workspaceResponse = {
+ value: [
+ {
+ name: 'aworkspace',
+ properties: {
+ source: 'Azure',
+ customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
+ },
+ },
+ ],
+ };
+
+ let annotationResults;
+
+ beforeEach(async () => {
+ ctx.backendSrv.datasourceRequest = options => {
+ if (options.url.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
+ return ctx.$q.when({ data: workspaceResponse, status: 200 });
+ } else {
+ return ctx.$q.when({ data: tableResponse, status: 200 });
+ }
+ };
+
+ annotationResults = await ctx.ds.annotationQuery({
+ annotation: {
+ rawQuery: 'Heartbeat | where $__timeFilter()| project TimeGenerated, Text=Computer, tags="test"',
+ workspace: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
+ },
+ range: {
+ from: moment.utc('2017-08-22T20:00:00Z'),
+ to: moment.utc('2017-08-22T23:59:00Z'),
+ },
+ rangeRaw: {
+ from: 'now-4h',
+ to: 'now',
+ },
+ });
+ });
+
+ it('should return a list of categories in the correct format', () => {
+ expect(annotationResults.length).toBe(2);
+
+ expect(annotationResults[0].time).toBe(1527970800000);
+ expect(annotationResults[0].text).toBe('Computer1');
+ expect(annotationResults[0].tags[0]).toBe('tag1');
+ expect(annotationResults[0].tags[1]).toBe('tag2');
+
+ expect(annotationResults[1].time).toBe(1527971280000);
+ expect(annotationResults[1].text).toBe('Computer2');
+ expect(annotationResults[1].tags[0]).toBe('tag2');
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts
new file mode 100644
index 00000000000..9a5a1e259ec
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/azure_log_analytics_datasource.ts
@@ -0,0 +1,321 @@
+import _ from 'lodash';
+import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
+import ResponseParser from './response_parser';
+
+export default class AzureLogAnalyticsDatasource {
+ id: number;
+ url: string;
+ baseUrl: string;
+ applicationId: string;
+ azureMonitorUrl: string;
+ defaultOrFirstWorkspace: string;
+ subscriptionId: string;
+
+ /** @ngInject */
+ constructor(private instanceSettings, private backendSrv, private templateSrv, private $q) {
+ this.id = instanceSettings.id;
+ this.baseUrl = this.instanceSettings.jsonData.azureLogAnalyticsSameAs
+ ? '/sameasloganalyticsazure'
+ : `/loganalyticsazure`;
+ this.url = instanceSettings.url;
+ this.defaultOrFirstWorkspace = this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace;
+
+ this.setWorkspaceUrl();
+ }
+
+ isConfigured(): boolean {
+ return (
+ (!!this.instanceSettings.jsonData.logAnalyticsSubscriptionId &&
+ this.instanceSettings.jsonData.logAnalyticsSubscriptionId.length > 0) ||
+ !!this.instanceSettings.jsonData.azureLogAnalyticsSameAs
+ );
+ }
+
+ setWorkspaceUrl() {
+ if (!!this.instanceSettings.jsonData.subscriptionId || !!this.instanceSettings.jsonData.azureLogAnalyticsSameAs) {
+ this.subscriptionId = this.instanceSettings.jsonData.subscriptionId;
+ const azureCloud = this.instanceSettings.jsonData.cloudName || 'azuremonitor';
+ this.azureMonitorUrl = `/${azureCloud}/subscriptions/${this.subscriptionId}`;
+ } else {
+ this.subscriptionId = this.instanceSettings.jsonData.logAnalyticsSubscriptionId;
+ this.azureMonitorUrl = `/workspacesloganalytics/subscriptions/${this.subscriptionId}`;
+ }
+ }
+
+ getWorkspaces() {
+ const workspaceListUrl =
+ this.azureMonitorUrl + '/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview';
+ return this.doRequest(workspaceListUrl).then(response => {
+ return (
+ _.map(response.data.value, val => {
+ return { text: val.name, value: val.properties.customerId };
+ }) || []
+ );
+ });
+ }
+
+ getSchema(workspace) {
+ if (!workspace) {
+ return Promise.resolve();
+ }
+ const url = `${this.baseUrl}/${workspace}/metadata`;
+
+ return this.doRequest(url).then(response => {
+ return new ResponseParser(response.data).parseSchemaResult();
+ });
+ }
+
+ query(options) {
+ const queries = _.filter(options.targets, item => {
+ return item.hide !== true;
+ }).map(target => {
+ const item = target.azureLogAnalytics;
+
+ const querystringBuilder = new LogAnalyticsQuerystringBuilder(
+ this.templateSrv.replace(item.query, options.scopedVars, this.interpolateVariable),
+ options,
+ 'TimeGenerated'
+ );
+ const generated = querystringBuilder.generate();
+
+ const url = `${this.baseUrl}/${item.workspace}/query?${generated.uriString}`;
+
+ return {
+ refId: target.refId,
+ intervalMs: options.intervalMs,
+ maxDataPoints: options.maxDataPoints,
+ datasourceId: this.id,
+ url: url,
+ query: generated.rawQuery,
+ format: options.format,
+ resultFormat: item.resultFormat,
+ };
+ });
+
+ if (!queries || queries.length === 0) {
+ return;
+ }
+
+ const promises = this.doQueries(queries);
+
+ return this.$q.all(promises).then(results => {
+ return new ResponseParser(results).parseQueryResult();
+ });
+ }
+
+ metricFindQuery(query: string) {
+ return this.getDefaultOrFirstWorkspace().then(workspace => {
+ const queries: any[] = this.buildQuery(query, null, workspace);
+
+ const promises = this.doQueries(queries);
+
+ return this.$q
+ .all(promises)
+ .then(results => {
+ return new ResponseParser(results).parseToVariables();
+ })
+ .catch(err => {
+ if (
+ err.error &&
+ err.error.data &&
+ err.error.data.error &&
+ err.error.data.error.innererror &&
+ err.error.data.error.innererror.innererror
+ ) {
+ throw { message: err.error.data.error.innererror.innererror.message };
+ } else if (err.error && err.error.data && err.error.data.error) {
+ throw { message: err.error.data.error.message };
+ }
+ });
+ });
+ }
+
+ private buildQuery(query: string, options: any, workspace: any) {
+ const querystringBuilder = new LogAnalyticsQuerystringBuilder(
+ this.templateSrv.replace(query, {}, this.interpolateVariable),
+ options,
+ 'TimeGenerated'
+ );
+ const querystring = querystringBuilder.generate().uriString;
+ const url = `${this.baseUrl}/${workspace}/query?${querystring}`;
+ const queries: any[] = [];
+ queries.push({
+ datasourceId: this.id,
+ url: url,
+ resultFormat: 'table',
+ });
+ return queries;
+ }
+
+ interpolateVariable(value, variable) {
+ if (typeof value === 'string') {
+ if (variable.multi || variable.includeAll) {
+ return "'" + value + "'";
+ } else {
+ return value;
+ }
+ }
+
+ if (typeof value === 'number') {
+ return value;
+ }
+
+ const quotedValues = _.map(value, val => {
+ if (typeof value === 'number') {
+ return value;
+ }
+
+ return "'" + val + "'";
+ });
+ return quotedValues.join(',');
+ }
+
+ getDefaultOrFirstWorkspace() {
+ if (this.defaultOrFirstWorkspace) {
+ return Promise.resolve(this.defaultOrFirstWorkspace);
+ }
+
+ return this.getWorkspaces().then(workspaces => {
+ this.defaultOrFirstWorkspace = workspaces[0].value;
+ return this.defaultOrFirstWorkspace;
+ });
+ }
+
+ annotationQuery(options) {
+ if (!options.annotation.rawQuery) {
+ return this.$q.reject({
+ message: 'Query missing in annotation definition',
+ });
+ }
+
+ const queries: any[] = this.buildQuery(options.annotation.rawQuery, options, options.annotation.workspace);
+
+ const promises = this.doQueries(queries);
+
+ return this.$q.all(promises).then(results => {
+ const annotations = new ResponseParser(results).transformToAnnotations(options);
+ return annotations;
+ });
+ }
+
+ doQueries(queries) {
+ return _.map(queries, query => {
+ return this.doRequest(query.url)
+ .then(result => {
+ return {
+ result: result,
+ query: query,
+ };
+ })
+ .catch(err => {
+ throw {
+ error: err,
+ query: query,
+ };
+ });
+ });
+ }
+
+ doRequest(url, maxRetries = 1) {
+ return this.backendSrv
+ .datasourceRequest({
+ url: this.url + url,
+ method: 'GET',
+ })
+ .catch(error => {
+ if (maxRetries > 0) {
+ return this.doRequest(url, maxRetries - 1);
+ }
+
+ throw error;
+ });
+ }
+
+ testDatasource() {
+ const validationError = this.isValidConfig();
+ if (validationError) {
+ return validationError;
+ }
+
+ return this.getDefaultOrFirstWorkspace()
+ .then(ws => {
+ const url = `${this.baseUrl}/${ws}/metadata`;
+
+ return this.doRequest(url);
+ })
+ .then(response => {
+ if (response.status === 200) {
+ return {
+ status: 'success',
+ message: 'Successfully queried the Azure Log Analytics service.',
+ title: 'Success',
+ };
+ }
+
+ return {
+ status: 'error',
+ message: 'Returned http status code ' + response.status,
+ };
+ })
+ .catch(error => {
+ let message = 'Azure Log Analytics: ';
+ if (error.config && error.config.url && error.config.url.indexOf('workspacesloganalytics') > -1) {
+ message = 'Azure Log Analytics requires access to Azure Monitor but had the following error: ';
+ }
+
+ message = this.getErrorMessage(message, error);
+
+ return {
+ status: 'error',
+ message: message,
+ };
+ });
+ }
+
+ private getErrorMessage(message: string, error: any) {
+ message += error.statusText ? error.statusText + ': ' : '';
+ if (error.data && error.data.error && error.data.error.code) {
+ message += error.data.error.code + '. ' + error.data.error.message;
+ } else if (error.data && error.data.error) {
+ message += error.data.error;
+ } else if (error.data) {
+ message += error.data;
+ } else {
+ message += 'Cannot connect to Azure Log Analytics REST API.';
+ }
+ return message;
+ }
+
+ isValidConfig() {
+ if (this.instanceSettings.jsonData.azureLogAnalyticsSameAs) {
+ return undefined;
+ }
+
+ if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsSubscriptionId)) {
+ return {
+ status: 'error',
+ message: 'The Subscription Id field is required.',
+ };
+ }
+
+ if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsTenantId)) {
+ return {
+ status: 'error',
+ message: 'The Tenant Id field is required.',
+ };
+ }
+
+ if (!this.isValidConfigField(this.instanceSettings.jsonData.logAnalyticsClientId)) {
+ return {
+ status: 'error',
+ message: 'The Client Id field is required.',
+ };
+ }
+
+ return undefined;
+ }
+
+ isValidConfigField(field: string) {
+ return field && field.length > 0;
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts
new file mode 100644
index 00000000000..e747c2e4d70
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_log_analytics/response_parser.ts
@@ -0,0 +1,269 @@
+import _ from 'lodash';
+import moment from 'moment';
+
+export interface DataTarget {
+ target: string;
+ datapoints: any[];
+ refId: string;
+ query: any;
+}
+export interface TableResult {
+ columns: TableColumn[];
+ rows: any[];
+ type: string;
+ refId: string;
+ query: string;
+}
+
+export interface TableColumn {
+ text: string;
+ type: string;
+}
+
+export interface KustoSchema {
+ Databases: { [key: string]: KustoDatabase };
+ Plugins: any[];
+}
+export interface KustoDatabase {
+ Name: string;
+ Tables: { [key: string]: KustoTable };
+ Functions: { [key: string]: KustoFunction };
+}
+
+export interface KustoTable {
+ Name: string;
+ OrderedColumns: KustoColumn[];
+}
+
+export interface KustoColumn {
+ Name: string;
+ Type: string;
+}
+
+export interface KustoFunction {
+ Name: string;
+ DocString: string;
+ Body: string;
+ Folder: string;
+ FunctionKind: string;
+ InputParameters: any[];
+ OutputColumns: any[];
+}
+
+export interface Variable {
+ text: string;
+ value: string;
+}
+
+export interface AnnotationItem {
+ annotation: any;
+ time: number;
+ text: string;
+ tags: string[];
+}
+
+export default class ResponseParser {
+ columns: string[];
+ constructor(private results) {}
+
+ parseQueryResult(): any {
+ let data: any[] = [];
+ let columns: any[] = [];
+ for (let i = 0; i < this.results.length; i++) {
+ if (this.results[i].result.data.tables.length === 0) {
+ continue;
+ }
+ columns = this.results[i].result.data.tables[0].columns;
+ const rows = this.results[i].result.data.tables[0].rows;
+
+ if (this.results[i].query.resultFormat === 'time_series') {
+ data = _.concat(data, this.parseTimeSeriesResult(this.results[i].query, columns, rows));
+ } else {
+ data = _.concat(data, this.parseTableResult(this.results[i].query, columns, rows));
+ }
+ }
+
+ return data;
+ }
+
+ parseTimeSeriesResult(query, columns, rows): DataTarget[] {
+ const data: DataTarget[] = [];
+ let timeIndex = -1;
+ let metricIndex = -1;
+ let valueIndex = -1;
+
+ for (let i = 0; i < columns.length; i++) {
+ if (timeIndex === -1 && columns[i].type === 'datetime') {
+ timeIndex = i;
+ }
+
+ if (metricIndex === -1 && columns[i].type === 'string') {
+ metricIndex = i;
+ }
+
+ if (valueIndex === -1 && ['int', 'long', 'real', 'double'].indexOf(columns[i].type) > -1) {
+ valueIndex = i;
+ }
+ }
+
+ if (timeIndex === -1) {
+ throw new Error('No datetime column found in the result. The Time Series format requires a time column.');
+ }
+
+ _.forEach(rows, row => {
+ const epoch = ResponseParser.dateTimeToEpoch(row[timeIndex]);
+ const metricName = metricIndex > -1 ? row[metricIndex] : columns[valueIndex].name;
+ const bucket = ResponseParser.findOrCreateBucket(data, metricName);
+ bucket.datapoints.push([row[valueIndex], epoch]);
+ bucket.refId = query.refId;
+ bucket.query = query.query;
+ });
+
+ return data;
+ }
+
+ parseTableResult(query, columns, rows): TableResult {
+ const tableResult: TableResult = {
+ type: 'table',
+ columns: _.map(columns, col => {
+ return { text: col.name, type: col.type };
+ }),
+ rows: rows,
+ refId: query.refId,
+ query: query.query,
+ };
+
+ return tableResult;
+ }
+
+ parseToVariables(): Variable[] {
+ const queryResult = this.parseQueryResult();
+
+ const variables: Variable[] = [];
+ _.forEach(queryResult, result => {
+ _.forEach(_.flattenDeep(result.rows), row => {
+ variables.push({
+ text: row,
+ value: row,
+ } as Variable);
+ });
+ });
+
+ return variables;
+ }
+
+ transformToAnnotations(options: any) {
+ const queryResult = this.parseQueryResult();
+
+ const list: AnnotationItem[] = [];
+
+ _.forEach(queryResult, result => {
+ let timeIndex = -1;
+ let textIndex = -1;
+ let tagsIndex = -1;
+
+ for (let i = 0; i < result.columns.length; i++) {
+ if (timeIndex === -1 && result.columns[i].type === 'datetime') {
+ timeIndex = i;
+ }
+
+ if (textIndex === -1 && result.columns[i].text.toLowerCase() === 'text') {
+ textIndex = i;
+ }
+
+ if (tagsIndex === -1 && result.columns[i].text.toLowerCase() === 'tags') {
+ tagsIndex = i;
+ }
+ }
+
+ _.forEach(result.rows, row => {
+ list.push({
+ annotation: options.annotation,
+ time: Math.floor(ResponseParser.dateTimeToEpoch(row[timeIndex])),
+ text: row[textIndex] ? row[textIndex].toString() : '',
+ tags: row[tagsIndex] ? row[tagsIndex].trim().split(/\s*,\s*/) : [],
+ });
+ });
+ });
+
+ return list;
+ }
+
+ parseSchemaResult(): KustoSchema {
+ return {
+ Plugins: [
+ {
+ Name: 'pivot',
+ },
+ ],
+ Databases: this.createSchemaDatabaseWithTables(),
+ };
+ }
+
+ createSchemaDatabaseWithTables(): { [key: string]: KustoDatabase } {
+ const databases = {
+ Default: {
+ Name: 'Default',
+ Tables: this.createSchemaTables(),
+ Functions: this.createSchemaFunctions(),
+ },
+ };
+
+ return databases;
+ }
+
+ createSchemaTables(): { [key: string]: KustoTable } {
+ const tables: { [key: string]: KustoTable } = {};
+
+ for (const table of this.results.tables) {
+ tables[table.name] = {
+ Name: table.name,
+ OrderedColumns: [],
+ };
+ for (const col of table.columns) {
+ tables[table.name].OrderedColumns.push(this.convertToKustoColumn(col));
+ }
+ }
+
+ return tables;
+ }
+
+ convertToKustoColumn(col: any): KustoColumn {
+ return {
+ Name: col.name,
+ Type: col.type,
+ };
+ }
+
+ createSchemaFunctions(): { [key: string]: KustoFunction } {
+ const functions: { [key: string]: KustoFunction } = {};
+
+ for (const func of this.results.functions) {
+ functions[func.name] = {
+ Name: func.name,
+ Body: func.body,
+ DocString: func.displayName,
+ Folder: func.category,
+ FunctionKind: 'Unknown',
+ InputParameters: [],
+ OutputColumns: [],
+ };
+ }
+
+ return functions;
+ }
+
+ static findOrCreateBucket(data, target): DataTarget {
+ let dataTarget = _.find(data, ['target', target]);
+ if (!dataTarget) {
+ dataTarget = { target: target, datapoints: [], refId: '', query: '' };
+ data.push(dataTarget);
+ }
+
+ return dataTarget;
+ }
+
+ static dateTimeToEpoch(dateTime) {
+ return moment(dateTime).valueOf();
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
new file mode 100644
index 00000000000..c585724990a
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.test.ts
@@ -0,0 +1,819 @@
+import AzureMonitorDatasource from '../datasource';
+import Q from 'q';
+import moment from 'moment';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+describe('AzureMonitorDatasource', () => {
+ const ctx: any = {
+ backendSrv: {},
+ templateSrv: new TemplateSrv(),
+ };
+
+ beforeEach(() => {
+ ctx.$q = Q;
+ ctx.instanceSettings = {
+ url: 'http://azuremonitor.com',
+ jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f' },
+ cloudName: 'azuremonitor',
+ };
+
+ ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings, ctx.backendSrv, ctx.templateSrv, ctx.$q);
+ });
+
+ describe('When performing testDatasource', () => {
+ describe('and an error is returned', () => {
+ const error = {
+ data: {
+ error: {
+ code: 'InvalidApiVersionParameter',
+ message: `An error message.`,
+ },
+ },
+ status: 400,
+ statusText: 'Bad Request',
+ };
+
+ beforeEach(() => {
+ ctx.instanceSettings.jsonData.tenantId = 'xxx';
+ ctx.instanceSettings.jsonData.clientId = 'xxx';
+ ctx.backendSrv.datasourceRequest = options => {
+ return ctx.$q.reject(error);
+ };
+ });
+
+ it('should return error status and a detailed error message', () => {
+ return ctx.ds.testDatasource().then(results => {
+ expect(results.status).toEqual('error');
+ expect(results.message).toEqual(
+ '1. Azure Monitor: Bad Request: InvalidApiVersionParameter. An error message. '
+ );
+ });
+ });
+ });
+
+ describe('and a list of resource groups is returned', () => {
+ const response = {
+ data: {
+ value: [{ name: 'grp1' }, { name: 'grp2' }],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.instanceSettings.jsonData.tenantId = 'xxx';
+ ctx.instanceSettings.jsonData.clientId = 'xxx';
+ ctx.backendSrv.datasourceRequest = options => {
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return success status', () => {
+ return ctx.ds.testDatasource().then(results => {
+ expect(results.status).toEqual('success');
+ });
+ });
+ });
+ });
+
+ describe('When performing query', () => {
+ const options = {
+ range: {
+ from: moment.utc('2017-08-22T20:00:00Z'),
+ to: moment.utc('2017-08-22T23:59:00Z'),
+ },
+ targets: [
+ {
+ apiVersion: '2018-01-01',
+ refId: 'A',
+ queryType: 'Azure Monitor',
+ azureMonitor: {
+ resourceGroup: 'testRG',
+ resourceName: 'testRN',
+ metricDefinition: 'Microsoft.Compute/virtualMachines',
+ metricName: 'Percentage CPU',
+ timeGrain: 'PT1H',
+ alias: '',
+ },
+ },
+ ],
+ };
+
+ describe('and data field is average', () => {
+ const response = {
+ value: [
+ {
+ timeseries: [
+ {
+ data: [
+ {
+ timeStamp: '2017-08-22T21:00:00Z',
+ average: 1.0503333333333331,
+ },
+ {
+ timeStamp: '2017-08-22T22:00:00Z',
+ average: 1.045083333333333,
+ },
+ {
+ timeStamp: '2017-08-22T23:00:00Z',
+ average: 1.0457499999999995,
+ },
+ ],
+ },
+ ],
+ id:
+ '/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' +
+ '/testRN/providers/Microsoft.Insights/metrics/Percentage CPU',
+ name: {
+ value: 'Percentage CPU',
+ localizedValue: 'Percentage CPU',
+ },
+ type: 'Microsoft.Insights/metrics',
+ unit: 'Percent',
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain(
+ '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'
+ );
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(1);
+ expect(results.data[0].target).toEqual('testRN.Percentage CPU');
+ expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
+ expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
+ expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
+ expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
+ });
+ });
+ });
+
+ describe('and data field is total', () => {
+ const response = {
+ value: [
+ {
+ timeseries: [
+ {
+ data: [
+ {
+ timeStamp: '2017-08-22T21:00:00Z',
+ total: 1.0503333333333331,
+ },
+ {
+ timeStamp: '2017-08-22T22:00:00Z',
+ total: 1.045083333333333,
+ },
+ {
+ timeStamp: '2017-08-22T23:00:00Z',
+ total: 1.0457499999999995,
+ },
+ ],
+ },
+ ],
+ id:
+ '/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' +
+ '/testRN/providers/Microsoft.Insights/metrics/Percentage CPU',
+ name: {
+ value: 'Percentage CPU',
+ localizedValue: 'Percentage CPU',
+ },
+ type: 'Microsoft.Insights/metrics',
+ unit: 'Percent',
+ },
+ ],
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ expect(options.url).toContain(
+ '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics'
+ );
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(1);
+ expect(results.data[0].target).toEqual('testRN.Percentage CPU');
+ expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
+ expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
+ expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
+ expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
+ });
+ });
+ });
+
+ describe('and data has a dimension filter', () => {
+ const response = {
+ value: [
+ {
+ timeseries: [
+ {
+ data: [
+ {
+ timeStamp: '2017-08-22T21:00:00Z',
+ total: 1.0503333333333331,
+ },
+ {
+ timeStamp: '2017-08-22T22:00:00Z',
+ total: 1.045083333333333,
+ },
+ {
+ timeStamp: '2017-08-22T23:00:00Z',
+ total: 1.0457499999999995,
+ },
+ ],
+ metadatavalues: [
+ {
+ name: {
+ value: 'blobtype',
+ localizedValue: 'blobtype',
+ },
+ value: 'BlockBlob',
+ },
+ ],
+ },
+ ],
+ id:
+ '/subscriptions/xxx/resourceGroups/testRG/providers/Microsoft.Compute/virtualMachines' +
+ '/testRN/providers/Microsoft.Insights/metrics/Percentage CPU',
+ name: {
+ value: 'Percentage CPU',
+ localizedValue: 'Percentage CPU',
+ },
+ type: 'Microsoft.Insights/metrics',
+ unit: 'Percent',
+ },
+ ],
+ };
+
+ describe('and with no alias specified', () => {
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const expected =
+ '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics';
+ expect(options.url).toContain(expected);
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(1);
+ expect(results.data[0].target).toEqual('testRN{blobtype=BlockBlob}.Percentage CPU');
+ expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
+ expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
+ expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
+ expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
+ });
+ });
+ });
+
+ describe('and with an alias specified', () => {
+ beforeEach(() => {
+ options.targets[0].azureMonitor.alias =
+ '{{resourcegroup}} + {{namespace}} + {{resourcename}} + ' +
+ '{{metric}} + {{dimensionname}} + {{dimensionvalue}}';
+
+ ctx.backendSrv.datasourceRequest = options => {
+ const expected =
+ '/testRG/providers/Microsoft.Compute/virtualMachines/testRN/providers/microsoft.insights/metrics';
+ expect(options.url).toContain(expected);
+ return ctx.$q.when({ data: response, status: 200 });
+ };
+ });
+
+ it('should return a list of datapoints', () => {
+ return ctx.ds.query(options).then(results => {
+ expect(results.data.length).toBe(1);
+ const expected =
+ 'testRG + Microsoft.Compute/virtualMachines + testRN + Percentage CPU + blobtype + BlockBlob';
+ expect(results.data[0].target).toEqual(expected);
+ expect(results.data[0].datapoints[0][1]).toEqual(1503435600000);
+ expect(results.data[0].datapoints[0][0]).toEqual(1.0503333333333331);
+ expect(results.data[0].datapoints[2][1]).toEqual(1503442800000);
+ expect(results.data[0].datapoints[2][0]).toEqual(1.0457499999999995);
+ });
+ });
+ });
+ });
+ });
+
+ describe('When performing metricFindQuery', () => {
+ describe('with a metric names query', () => {
+ const response = {
+ data: {
+ value: [{ name: 'grp1' }, { name: 'grp2' }],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return a list of metric names', () => {
+ return ctx.ds.metricFindQuery('ResourceGroups()').then(results => {
+ expect(results.length).toBe(2);
+ expect(results[0].text).toBe('grp1');
+ expect(results[0].value).toBe('grp1');
+ expect(results[1].text).toBe('grp2');
+ expect(results[1].value).toBe('grp2');
+ });
+ });
+ });
+
+ describe('with metric definitions query', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: 'test',
+ type: 'Microsoft.Network/networkInterfaces',
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
+ expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return a list of metric definitions', () => {
+ return ctx.ds.metricFindQuery('Namespaces(nodesapp)').then(results => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('Microsoft.Network/networkInterfaces');
+ expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
+ });
+ });
+ });
+
+ describe('with resource names query', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: 'Failure Anomalies - nodeapp',
+ type: 'microsoft.insights/alertrules',
+ },
+ {
+ name: 'nodeapp',
+ type: 'microsoft.insights/components',
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
+ expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return a list of resource names', () => {
+ return ctx.ds.metricFindQuery('resourceNames(nodeapp, microsoft.insights/components )').then(results => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('nodeapp');
+ expect(results[0].value).toEqual('nodeapp');
+ });
+ });
+ });
+
+ describe('with metric names query', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: {
+ value: 'Percentage CPU',
+ localizedValue: 'Percentage CPU',
+ },
+ },
+ {
+ name: {
+ value: 'UsedCapacity',
+ localizedValue: 'Used capacity',
+ },
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
+ expect(options.url).toBe(
+ baseUrl +
+ '/nodeapp/providers/microsoft.insights/components/rn/providers/microsoft.insights/' +
+ 'metricdefinitions?api-version=2018-01-01'
+ );
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return a list of metric names', () => {
+ return ctx.ds.metricFindQuery('Metricnames(nodeapp, microsoft.insights/components, rn)').then(results => {
+ expect(results.length).toEqual(2);
+ expect(results[0].text).toEqual('Percentage CPU');
+ expect(results[0].value).toEqual('Percentage CPU');
+
+ expect(results[1].text).toEqual('Used capacity');
+ expect(results[1].value).toEqual('UsedCapacity');
+ });
+ });
+ });
+ });
+
+ describe('When performing getResourceGroups', () => {
+ const response = {
+ data: {
+ value: [{ name: 'grp1' }, { name: 'grp2' }],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return list of Resource Groups', () => {
+ return ctx.ds.getResourceGroups().then(results => {
+ expect(results.length).toEqual(2);
+ expect(results[0].text).toEqual('grp1');
+ expect(results[0].value).toEqual('grp1');
+ expect(results[1].text).toEqual('grp2');
+ expect(results[1].value).toEqual('grp2');
+ });
+ });
+ });
+
+ describe('When performing getMetricDefinitions', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: 'test',
+ type: 'Microsoft.Network/networkInterfaces',
+ },
+ {
+ location: 'northeurope',
+ name: 'northeur',
+ type: 'Microsoft.Compute/virtualMachines',
+ },
+ {
+ location: 'westcentralus',
+ name: 'us',
+ type: 'Microsoft.Compute/virtualMachines',
+ },
+ {
+ name: 'IHaveNoMetrics',
+ type: 'IShouldBeFilteredOut',
+ },
+ {
+ name: 'storageTest',
+ type: 'Microsoft.Storage/storageAccounts',
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
+ expect(options.url).toBe(baseUrl + '/nodesapp/resources?api-version=2018-01-01');
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
+ return ctx.ds.getMetricDefinitions('nodesapp').then(results => {
+ expect(results.length).toEqual(7);
+ expect(results[0].text).toEqual('Microsoft.Network/networkInterfaces');
+ expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
+ expect(results[1].text).toEqual('Microsoft.Compute/virtualMachines');
+ expect(results[1].value).toEqual('Microsoft.Compute/virtualMachines');
+ expect(results[2].text).toEqual('Microsoft.Storage/storageAccounts');
+ expect(results[2].value).toEqual('Microsoft.Storage/storageAccounts');
+ expect(results[3].text).toEqual('Microsoft.Storage/storageAccounts/blobServices');
+ expect(results[3].value).toEqual('Microsoft.Storage/storageAccounts/blobServices');
+ expect(results[4].text).toEqual('Microsoft.Storage/storageAccounts/fileServices');
+ expect(results[4].value).toEqual('Microsoft.Storage/storageAccounts/fileServices');
+ expect(results[5].text).toEqual('Microsoft.Storage/storageAccounts/tableServices');
+ expect(results[5].value).toEqual('Microsoft.Storage/storageAccounts/tableServices');
+ expect(results[6].text).toEqual('Microsoft.Storage/storageAccounts/queueServices');
+ expect(results[6].value).toEqual('Microsoft.Storage/storageAccounts/queueServices');
+ });
+ });
+ });
+
+ describe('When performing getResourceNames', () => {
+ describe('and there are no special cases', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: 'Failure Anomalies - nodeapp',
+ type: 'microsoft.insights/alertrules',
+ },
+ {
+ name: 'nodeapp',
+ type: 'microsoft.insights/components',
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
+ expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return list of Resource Names', () => {
+ return ctx.ds.getResourceNames('nodeapp', 'microsoft.insights/components').then(results => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('nodeapp');
+ expect(results[0].value).toEqual('nodeapp');
+ });
+ });
+ });
+
+ describe('and the metric definition is blobServices', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: 'Failure Anomalies - nodeapp',
+ type: 'microsoft.insights/alertrules',
+ },
+ {
+ name: 'storagetest',
+ type: 'Microsoft.Storage/storageAccounts',
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
+ expect(options.url).toBe(baseUrl + '/nodeapp/resources?api-version=2018-01-01');
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return list of Resource Names', () => {
+ return ctx.ds.getResourceNames('nodeapp', 'Microsoft.Storage/storageAccounts/blobServices').then(results => {
+ expect(results.length).toEqual(1);
+ expect(results[0].text).toEqual('storagetest/default');
+ expect(results[0].value).toEqual('storagetest/default');
+ });
+ });
+ });
+ });
+
+ describe('When performing getMetricNames', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: {
+ value: 'UsedCapacity',
+ localizedValue: 'Used capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Total',
+ supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
+ },
+ {
+ name: {
+ value: 'FreeCapacity',
+ localizedValue: 'Free capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Average',
+ supportedAggregationTypes: ['None', 'Average'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
+ const expected =
+ baseUrl +
+ '/providers/microsoft.insights/components/resource1' +
+ '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01';
+ expect(options.url).toBe(expected);
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return list of Metric Definitions', () => {
+ return ctx.ds.getMetricNames('nodeapp', 'microsoft.insights/components', 'resource1').then(results => {
+ expect(results.length).toEqual(2);
+ expect(results[0].text).toEqual('Used capacity');
+ expect(results[0].value).toEqual('UsedCapacity');
+ expect(results[1].text).toEqual('Free capacity');
+ expect(results[1].value).toEqual('FreeCapacity');
+ });
+ });
+ });
+
+ describe('When performing getMetricMetadata', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: {
+ value: 'UsedCapacity',
+ localizedValue: 'Used capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Total',
+ supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
+ },
+ {
+ name: {
+ value: 'FreeCapacity',
+ localizedValue: 'Free capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Average',
+ supportedAggregationTypes: ['None', 'Average'],
+ metricAvailabilities: [
+ { timeGrain: 'PT1H', retention: 'P93D' },
+ { timeGrain: 'PT6H', retention: 'P93D' },
+ { timeGrain: 'PT12H', retention: 'P93D' },
+ { timeGrain: 'P1D', retention: 'P93D' },
+ ],
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
+ const expected =
+ baseUrl +
+ '/providers/microsoft.insights/components/resource1' +
+ '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01';
+ expect(options.url).toBe(expected);
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return Aggregation metadata for a Metric', () => {
+ return ctx.ds
+ .getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'UsedCapacity')
+ .then(results => {
+ expect(results.primaryAggType).toEqual('Total');
+ expect(results.supportedAggTypes.length).toEqual(6);
+ expect(results.supportedTimeGrains.length).toEqual(4);
+ });
+ });
+ });
+
+ describe('When performing getMetricMetadata on metrics with dimensions', () => {
+ const response = {
+ data: {
+ value: [
+ {
+ name: {
+ value: 'Transactions',
+ localizedValue: 'Transactions',
+ },
+ unit: 'Count',
+ primaryAggregationType: 'Total',
+ supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
+ isDimensionRequired: false,
+ dimensions: [
+ {
+ value: 'ResponseType',
+ localizedValue: 'Response type',
+ },
+ {
+ value: 'GeoType',
+ localizedValue: 'Geo type',
+ },
+ {
+ value: 'ApiName',
+ localizedValue: 'API name',
+ },
+ ],
+ },
+ {
+ name: {
+ value: 'FreeCapacity',
+ localizedValue: 'Free capacity',
+ },
+ unit: 'CountPerSecond',
+ primaryAggregationType: 'Average',
+ supportedAggregationTypes: ['None', 'Average'],
+ },
+ ],
+ },
+ status: 200,
+ statusText: 'OK',
+ };
+
+ beforeEach(() => {
+ ctx.backendSrv.datasourceRequest = options => {
+ const baseUrl =
+ 'http://azuremonitor.com/azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
+ const expected =
+ baseUrl +
+ '/providers/microsoft.insights/components/resource1' +
+ '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01';
+ expect(options.url).toBe(expected);
+ return ctx.$q.when(response);
+ };
+ });
+
+ it('should return dimensions for a Metric that has dimensions', () => {
+ return ctx.ds
+ .getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'Transactions')
+ .then(results => {
+ expect(results.dimensions.length).toEqual(4);
+ expect(results.dimensions[0].text).toEqual('None');
+ expect(results.dimensions[0].value).toEqual('None');
+ expect(results.dimensions[1].text).toEqual('Response type');
+ expect(results.dimensions[1].value).toEqual('ResponseType');
+ });
+ });
+
+ it('should return an empty array for a Metric that does not have dimensions', () => {
+ return ctx.ds
+ .getMetricMetadata('nodeapp', 'microsoft.insights/components', 'resource1', 'FreeCapacity')
+ .then(results => {
+ expect(results.dimensions.length).toEqual(0);
+ });
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts
new file mode 100644
index 00000000000..6f82ca72558
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_datasource.ts
@@ -0,0 +1,339 @@
+import _ from 'lodash';
+import AzureMonitorFilterBuilder from './azure_monitor_filter_builder';
+import UrlBuilder from './url_builder';
+import ResponseParser from './response_parser';
+import SupportedNamespaces from './supported_namespaces';
+import TimegrainConverter from '../time_grain_converter';
+
+export default class AzureMonitorDatasource {
+ apiVersion = '2018-01-01';
+ id: number;
+ subscriptionId: string;
+ baseUrl: string;
+ resourceGroup: string;
+ resourceName: string;
+ url: string;
+ defaultDropdownValue = 'select';
+ cloudName: string;
+ supportedMetricNamespaces: any[] = [];
+
+ /** @ngInject */
+ constructor(private instanceSettings, private backendSrv, private templateSrv, private $q) {
+ this.id = instanceSettings.id;
+ this.subscriptionId = instanceSettings.jsonData.subscriptionId;
+ this.cloudName = instanceSettings.jsonData.cloudName || 'azuremonitor';
+ this.baseUrl = `/${this.cloudName}/subscriptions/${this.subscriptionId}/resourceGroups`;
+ this.url = instanceSettings.url;
+
+ this.supportedMetricNamespaces = new SupportedNamespaces(this.cloudName).get();
+ }
+
+ isConfigured(): boolean {
+ return !!this.subscriptionId && this.subscriptionId.length > 0;
+ }
+
+ query(options) {
+ const queries = _.filter(options.targets, item => {
+ return (
+ item.hide !== true &&
+ item.azureMonitor.resourceGroup &&
+ item.azureMonitor.resourceGroup !== this.defaultDropdownValue &&
+ item.azureMonitor.resourceName &&
+ item.azureMonitor.resourceName !== this.defaultDropdownValue &&
+ item.azureMonitor.metricDefinition &&
+ item.azureMonitor.metricDefinition !== this.defaultDropdownValue &&
+ item.azureMonitor.metricName &&
+ item.azureMonitor.metricName !== this.defaultDropdownValue
+ );
+ }).map(target => {
+ const item = target.azureMonitor;
+
+ if (item.timeGrainUnit && item.timeGrain !== 'auto') {
+ item.timeGrain = TimegrainConverter.createISO8601Duration(item.timeGrain, item.timeGrainUnit);
+ }
+
+ const resourceGroup = this.templateSrv.replace(item.resourceGroup, options.scopedVars);
+ const resourceName = this.templateSrv.replace(item.resourceName, options.scopedVars);
+ const metricDefinition = this.templateSrv.replace(item.metricDefinition, options.scopedVars);
+ const timeGrain = this.templateSrv.replace((item.timeGrain || '').toString(), options.scopedVars);
+
+ const filterBuilder = new AzureMonitorFilterBuilder(
+ item.metricName,
+ options.range.from,
+ options.range.to,
+ timeGrain,
+ options.interval
+ );
+
+ if (item.timeGrains) {
+ filterBuilder.setAllowedTimeGrains(item.timeGrains);
+ }
+
+ if (item.aggregation) {
+ filterBuilder.setAggregation(item.aggregation);
+ }
+
+ if (item.dimension && item.dimension !== 'None') {
+ filterBuilder.setDimensionFilter(item.dimension, item.dimensionFilter);
+ }
+
+ const filter = this.templateSrv.replace(filterBuilder.generateFilter(), options.scopedVars);
+
+ const url = UrlBuilder.buildAzureMonitorQueryUrl(
+ this.baseUrl,
+ resourceGroup,
+ metricDefinition,
+ resourceName,
+ this.apiVersion,
+ filter
+ );
+
+ return {
+ refId: target.refId,
+ intervalMs: options.intervalMs,
+ maxDataPoints: options.maxDataPoints,
+ datasourceId: this.id,
+ url: url,
+ format: options.format,
+ alias: item.alias,
+ raw: false,
+ };
+ });
+
+ if (!queries || queries.length === 0) {
+ return;
+ }
+
+ const promises = this.doQueries(queries);
+
+ return this.$q.all(promises).then(results => {
+ return new ResponseParser(results).parseQueryResult();
+ });
+ }
+
+ doQueries(queries) {
+ return _.map(queries, query => {
+ return this.doRequest(query.url)
+ .then(result => {
+ return {
+ result: result,
+ query: query,
+ };
+ })
+ .catch(err => {
+ throw {
+ error: err,
+ query: query,
+ };
+ });
+ });
+ }
+
+ annotationQuery(options) {}
+
+ metricFindQuery(query: string) {
+ const resourceGroupsQuery = query.match(/^ResourceGroups\(\)/i);
+ if (resourceGroupsQuery) {
+ return this.getResourceGroups();
+ }
+
+ const metricDefinitionsQuery = query.match(/^Namespaces\(([^\)]+?)(,\s?([^,]+?))?\)/i);
+ if (metricDefinitionsQuery) {
+ return this.getMetricDefinitions(this.toVariable(metricDefinitionsQuery[1]));
+ }
+
+ const resourceNamesQuery = query.match(/^ResourceNames\(([^,]+?),\s?([^,]+?)\)/i);
+ if (resourceNamesQuery) {
+ const resourceGroup = this.toVariable(resourceNamesQuery[1]);
+ const metricDefinition = this.toVariable(resourceNamesQuery[2]);
+ return this.getResourceNames(resourceGroup, metricDefinition);
+ }
+
+ const metricNamesQuery = query.match(/^MetricNames\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/i);
+
+ if (metricNamesQuery) {
+ const resourceGroup = this.toVariable(metricNamesQuery[1]);
+ const metricDefinition = this.toVariable(metricNamesQuery[2]);
+ const resourceName = this.toVariable(metricNamesQuery[3]);
+ return this.getMetricNames(resourceGroup, metricDefinition, resourceName);
+ }
+
+ return undefined;
+ }
+
+ toVariable(metric: string) {
+ return this.templateSrv.replace((metric || '').trim());
+ }
+
+ getResourceGroups() {
+ const url = `${this.baseUrl}?api-version=${this.apiVersion}`;
+ return this.doRequest(url).then(result => {
+ return ResponseParser.parseResponseValues(result, 'name', 'name');
+ });
+ }
+
+ getMetricDefinitions(resourceGroup: string) {
+ const url = `${this.baseUrl}/${resourceGroup}/resources?api-version=${this.apiVersion}`;
+ return this.doRequest(url)
+ .then(result => {
+ return ResponseParser.parseResponseValues(result, 'type', 'type');
+ })
+ .then(result => {
+ return _.filter(result, t => {
+ for (let i = 0; i < this.supportedMetricNamespaces.length; i++) {
+ if (t.value.toLowerCase() === this.supportedMetricNamespaces[i].toLowerCase()) {
+ return true;
+ }
+ }
+
+ return false;
+ });
+ })
+ .then(result => {
+ let shouldHardcodeBlobStorage = false;
+ for (let i = 0; i < result.length; i++) {
+ if (result[i].value === 'Microsoft.Storage/storageAccounts') {
+ shouldHardcodeBlobStorage = true;
+ break;
+ }
+ }
+
+ if (shouldHardcodeBlobStorage) {
+ result.push({
+ text: 'Microsoft.Storage/storageAccounts/blobServices',
+ value: 'Microsoft.Storage/storageAccounts/blobServices',
+ });
+ result.push({
+ text: 'Microsoft.Storage/storageAccounts/fileServices',
+ value: 'Microsoft.Storage/storageAccounts/fileServices',
+ });
+ result.push({
+ text: 'Microsoft.Storage/storageAccounts/tableServices',
+ value: 'Microsoft.Storage/storageAccounts/tableServices',
+ });
+ result.push({
+ text: 'Microsoft.Storage/storageAccounts/queueServices',
+ value: 'Microsoft.Storage/storageAccounts/queueServices',
+ });
+ }
+
+ return result;
+ });
+ }
+
+ getResourceNames(resourceGroup: string, metricDefinition: string) {
+ const url = `${this.baseUrl}/${resourceGroup}/resources?api-version=${this.apiVersion}`;
+
+ return this.doRequest(url).then(result => {
+ if (!_.startsWith(metricDefinition, 'Microsoft.Storage/storageAccounts/')) {
+ return ResponseParser.parseResourceNames(result, metricDefinition);
+ }
+
+ const list = ResponseParser.parseResourceNames(result, 'Microsoft.Storage/storageAccounts');
+ for (let i = 0; i < list.length; i++) {
+ list[i].text += '/default';
+ list[i].value += '/default';
+ }
+
+ return list;
+ });
+ }
+
+ getMetricNames(resourceGroup: string, metricDefinition: string, resourceName: string) {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ this.baseUrl,
+ resourceGroup,
+ metricDefinition,
+ resourceName,
+ this.apiVersion
+ );
+
+ return this.doRequest(url).then(result => {
+ return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
+ });
+ }
+
+ getMetricMetadata(resourceGroup: string, metricDefinition: string, resourceName: string, metricName: string) {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ this.baseUrl,
+ resourceGroup,
+ metricDefinition,
+ resourceName,
+ this.apiVersion
+ );
+
+ return this.doRequest(url).then(result => {
+ return ResponseParser.parseMetadata(result, metricName);
+ });
+ }
+
+ testDatasource() {
+ if (!this.isValidConfigField(this.instanceSettings.jsonData.tenantId)) {
+ return {
+ status: 'error',
+ message: 'The Tenant Id field is required.',
+ };
+ }
+
+ if (!this.isValidConfigField(this.instanceSettings.jsonData.clientId)) {
+ return {
+ status: 'error',
+ message: 'The Client Id field is required.',
+ };
+ }
+
+ const url = `${this.baseUrl}?api-version=${this.apiVersion}`;
+ return this.doRequest(url)
+ .then(response => {
+ if (response.status === 200) {
+ return {
+ status: 'success',
+ message: 'Successfully queried the Azure Monitor service.',
+ title: 'Success',
+ };
+ }
+
+ return {
+ status: 'error',
+ message: 'Returned http status code ' + response.status,
+ };
+ })
+ .catch(error => {
+ let message = 'Azure Monitor: ';
+ message += error.statusText ? error.statusText + ': ' : '';
+
+ if (error.data && error.data.error && error.data.error.code) {
+ message += error.data.error.code + '. ' + error.data.error.message;
+ } else if (error.data && error.data.error) {
+ message += error.data.error;
+ } else if (error.data) {
+ message += error.data;
+ } else {
+ message += 'Cannot connect to Azure Monitor REST API.';
+ }
+ return {
+ status: 'error',
+ message: message,
+ };
+ });
+ }
+
+ isValidConfigField(field: string) {
+ return field && field.length > 0;
+ }
+
+ doRequest(url, maxRetries = 1) {
+ return this.backendSrv
+ .datasourceRequest({
+ url: this.url + url,
+ method: 'GET',
+ })
+ .catch(error => {
+ if (maxRetries > 0) {
+ return this.doRequest(url, maxRetries - 1);
+ }
+
+ throw error;
+ });
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts
new file mode 100644
index 00000000000..feaefbc5331
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.test.ts
@@ -0,0 +1,127 @@
+jest.mock('app/core/utils/kbn', () => {
+ return {
+ interval_to_ms: interval => {
+ if (interval.substring(interval.length - 1) === 's') {
+ return interval.substring(0, interval.length - 1) * 1000;
+ }
+
+ if (interval.substring(interval.length - 1) === 'm') {
+ return interval.substring(0, interval.length - 1) * 1000 * 60;
+ }
+
+ if (interval.substring(interval.length - 1) === 'd') {
+ return interval.substring(0, interval.length - 1) * 1000 * 60 * 24;
+ }
+
+ return undefined;
+ },
+ };
+});
+
+import AzureMonitorFilterBuilder from './azure_monitor_filter_builder';
+import moment from 'moment';
+
+describe('AzureMonitorFilterBuilder', () => {
+ let builder: AzureMonitorFilterBuilder;
+
+ const timefilter = 'timespan=2017-08-22T06:00:00Z/2017-08-22T07:00:00Z';
+ const metricFilter = 'metricnames=Percentage CPU';
+
+ beforeEach(() => {
+ builder = new AzureMonitorFilterBuilder(
+ 'Percentage CPU',
+ moment.utc('2017-08-22 06:00'),
+ moment.utc('2017-08-22 07:00'),
+ 'PT1H',
+ '3m'
+ );
+ });
+
+ describe('with a metric name and auto time grain of 3 minutes', () => {
+ beforeEach(() => {
+ builder.timeGrain = 'auto';
+ });
+
+ it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => {
+ const filter = timefilter + '&interval=PT5M&' + metricFilter;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+
+ describe('with a metric name and auto time grain of 30 seconds', () => {
+ beforeEach(() => {
+ builder.timeGrain = 'auto';
+ builder.grafanaInterval = '30s';
+ });
+
+ it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => {
+ const filter = timefilter + '&interval=PT1M&' + metricFilter;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+
+ describe('with a metric name and auto time grain of 10 minutes', () => {
+ beforeEach(() => {
+ builder.timeGrain = 'auto';
+ builder.grafanaInterval = '10m';
+ });
+
+ it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => {
+ const filter = timefilter + '&interval=PT15M&' + metricFilter;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+
+ describe('with a metric name and auto time grain of 2 day', () => {
+ beforeEach(() => {
+ builder.timeGrain = 'auto';
+ builder.grafanaInterval = '2d';
+ });
+
+ it('should always add datetime filtering and a time grain rounded to the closest allowed value to the filter', () => {
+ const filter = timefilter + '&interval=P1D&' + metricFilter;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+
+ describe('with a metric name and 1 hour time grain', () => {
+ it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => {
+ const filter = timefilter + '&interval=PT1H&' + metricFilter;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+
+ describe('with a metric name and 1 minute time grain', () => {
+ beforeEach(() => {
+ builder.timeGrain = 'PT1M';
+ });
+
+ it('should always add datetime filtering and a time grain in ISO_8601 format to the filter', () => {
+ const filter = timefilter + '&interval=PT1M&' + metricFilter;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+
+ describe('with a metric name and 1 day time grain and an aggregation', () => {
+ beforeEach(() => {
+ builder.timeGrain = 'P1D';
+ builder.setAggregation('Maximum');
+ });
+
+ it('should add time grain to the filter in ISO_8601 format', () => {
+ const filter = timefilter + '&interval=P1D&aggregation=Maximum&' + metricFilter;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+
+ describe('with a metric name and 1 day time grain and an aggregation and a dimension', () => {
+ beforeEach(() => {
+ builder.setDimensionFilter('aDimension', 'aFilterValue');
+ });
+
+ it('should add dimension to the filter', () => {
+ const filter = timefilter + '&interval=PT1H&' + metricFilter + `&$filter=aDimension eq 'aFilterValue'`;
+ expect(builder.generateFilter()).toEqual(filter);
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts
new file mode 100644
index 00000000000..1b04f868877
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/azure_monitor_filter_builder.ts
@@ -0,0 +1,72 @@
+import TimegrainConverter from '../time_grain_converter';
+
+export default class AzureMonitorFilterBuilder {
+ aggregation: string;
+ timeGrainInterval = '';
+ dimension: string;
+ dimensionFilter: string;
+ allowedTimeGrains = ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d'];
+
+ constructor(
+ private metricName: string,
+ private from,
+ private to,
+ public timeGrain: string,
+ public grafanaInterval: string
+ ) {}
+
+ setAllowedTimeGrains(timeGrains) {
+ this.allowedTimeGrains = [];
+ timeGrains.forEach(tg => {
+ if (tg.value === 'auto') {
+ this.allowedTimeGrains.push(tg.value);
+ } else {
+ this.allowedTimeGrains.push(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value));
+ }
+ });
+ }
+
+ setAggregation(agg) {
+ this.aggregation = agg;
+ }
+
+ setDimensionFilter(dimension, dimensionFilter) {
+ this.dimension = dimension;
+ this.dimensionFilter = dimensionFilter;
+ }
+
+ generateFilter() {
+ let filter = this.createDatetimeAndTimeGrainConditions();
+
+ if (this.aggregation) {
+ filter += `&aggregation=${this.aggregation}`;
+ }
+
+ if (this.metricName && this.metricName.trim().length > 0) {
+ filter += `&metricnames=${this.metricName}`;
+ }
+
+ if (this.dimension && this.dimensionFilter && this.dimensionFilter.trim().length > 0) {
+ filter += `&$filter=${this.dimension} eq '${this.dimensionFilter}'`;
+ }
+
+ return filter;
+ }
+
+ createDatetimeAndTimeGrainConditions() {
+ const dateTimeCondition = `timespan=${this.from.utc().format()}/${this.to.utc().format()}`;
+
+ if (this.timeGrain === 'auto') {
+ this.timeGrain = this.calculateAutoTimeGrain();
+ }
+ const timeGrainCondition = `&interval=${this.timeGrain}`;
+
+ return dateTimeCondition + timeGrainCondition;
+ }
+
+ calculateAutoTimeGrain() {
+ const roundedInterval = TimegrainConverter.findClosestTimeGrain(this.grafanaInterval, this.allowedTimeGrains);
+
+ return TimegrainConverter.createISO8601DurationFromInterval(roundedInterval);
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts
new file mode 100644
index 00000000000..ef882efbf99
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/response_parser.ts
@@ -0,0 +1,184 @@
+import moment from 'moment';
+import _ from 'lodash';
+import TimeGrainConverter from '../time_grain_converter';
+
+export default class ResponseParser {
+ constructor(private results) {}
+
+ parseQueryResult() {
+ const data: any[] = [];
+ for (let i = 0; i < this.results.length; i++) {
+ for (let j = 0; j < this.results[i].result.data.value.length; j++) {
+ for (let k = 0; k < this.results[i].result.data.value[j].timeseries.length; k++) {
+ const alias = this.results[i].query.alias;
+ data.push({
+ target: ResponseParser.createTarget(
+ this.results[i].result.data.value[j],
+ this.results[i].result.data.value[j].timeseries[k].metadatavalues,
+ alias
+ ),
+ datapoints: ResponseParser.convertDataToPoints(this.results[i].result.data.value[j].timeseries[k].data),
+ });
+ }
+ }
+ }
+ return data;
+ }
+
+ static createTarget(data, metadatavalues, alias: string) {
+ const resourceGroup = ResponseParser.parseResourceGroupFromId(data.id);
+ const resourceName = ResponseParser.parseResourceNameFromId(data.id);
+ const namespace = ResponseParser.parseNamespaceFromId(data.id, resourceName);
+ if (alias) {
+ const regex = /\{\{([\s\S]+?)\}\}/g;
+ return alias.replace(regex, (match, g1, g2) => {
+ const group = g1 || g2;
+
+ if (group === 'resourcegroup') {
+ return resourceGroup;
+ } else if (group === 'namespace') {
+ return namespace;
+ } else if (group === 'resourcename') {
+ return resourceName;
+ } else if (group === 'metric') {
+ return data.name.value;
+ } else if (group === 'dimensionname') {
+ return metadatavalues && metadatavalues.length > 0 ? metadatavalues[0].name.value : '';
+ } else if (group === 'dimensionvalue') {
+ return metadatavalues && metadatavalues.length > 0 ? metadatavalues[0].value : '';
+ }
+
+ return match;
+ });
+ }
+
+ if (metadatavalues && metadatavalues.length > 0) {
+ return `${resourceName}{${metadatavalues[0].name.value}=${metadatavalues[0].value}}.${data.name.value}`;
+ }
+
+ return `${resourceName}.${data.name.value}`;
+ }
+
+ static parseResourceGroupFromId(id: string) {
+ const startIndex = id.indexOf('/resourceGroups/') + 16;
+ const endIndex = id.indexOf('/providers');
+
+ return id.substring(startIndex, endIndex);
+ }
+
+ static parseNamespaceFromId(id: string, resourceName: string) {
+ const startIndex = id.indexOf('/providers/') + 11;
+ const endIndex = id.indexOf('/' + resourceName);
+
+ return id.substring(startIndex, endIndex);
+ }
+
+ static parseResourceNameFromId(id: string) {
+ const endIndex = id.lastIndexOf('/providers');
+ const startIndex = id.slice(0, endIndex).lastIndexOf('/') + 1;
+
+ return id.substring(startIndex, endIndex);
+ }
+
+ static convertDataToPoints(timeSeriesData) {
+ const dataPoints: any[] = [];
+
+ for (let k = 0; k < timeSeriesData.length; k++) {
+ const epoch = ResponseParser.dateTimeToEpoch(timeSeriesData[k].timeStamp);
+ const aggKey = ResponseParser.getKeyForAggregationField(timeSeriesData[k]);
+
+ if (aggKey) {
+ dataPoints.push([timeSeriesData[k][aggKey], epoch]);
+ }
+ }
+
+ return dataPoints;
+ }
+
+ static dateTimeToEpoch(dateTime) {
+ return moment(dateTime).valueOf();
+ }
+
+ static getKeyForAggregationField(dataObj): string {
+ const keys = _.keys(dataObj);
+ if (keys.length < 2) {
+ return '';
+ }
+
+ return _.intersection(keys, ['total', 'average', 'maximum', 'minimum', 'count'])[0];
+ }
+
+ static parseResponseValues(result: any, textFieldName: string, valueFieldName: string) {
+ const list: any[] = [];
+ for (let i = 0; i < result.data.value.length; i++) {
+ if (!_.find(list, ['value', _.get(result.data.value[i], valueFieldName)])) {
+ list.push({
+ text: _.get(result.data.value[i], textFieldName),
+ value: _.get(result.data.value[i], valueFieldName),
+ });
+ }
+ }
+ return list;
+ }
+
+ static parseResourceNames(result: any, metricDefinition: string) {
+ const list: any[] = [];
+ for (let i = 0; i < result.data.value.length; i++) {
+ if (result.data.value[i].type === metricDefinition) {
+ list.push({
+ text: result.data.value[i].name,
+ value: result.data.value[i].name,
+ });
+ }
+ }
+
+ return list;
+ }
+
+ static parseMetadata(result: any, metricName: string) {
+ const metricData = _.find(result.data.value, o => {
+ return _.get(o, 'name.value') === metricName;
+ });
+
+ const defaultAggTypes = ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'];
+
+ return {
+ primaryAggType: metricData.primaryAggregationType,
+ supportedAggTypes: metricData.supportedAggregationTypes || defaultAggTypes,
+ supportedTimeGrains: ResponseParser.parseTimeGrains(metricData.metricAvailabilities || []),
+ dimensions: ResponseParser.parseDimensions(metricData),
+ };
+ }
+
+ static parseTimeGrains(metricAvailabilities) {
+ const timeGrains: any[] = [];
+ metricAvailabilities.forEach(avail => {
+ if (avail.timeGrain) {
+ timeGrains.push({
+ text: TimeGrainConverter.createTimeGrainFromISO8601Duration(avail.timeGrain),
+ value: avail.timeGrain,
+ });
+ }
+ });
+ return timeGrains;
+ }
+
+ static parseDimensions(metricData: any) {
+ const dimensions: any[] = [];
+ if (!metricData.dimensions || metricData.dimensions.length === 0) {
+ return dimensions;
+ }
+
+ if (!metricData.isDimensionRequired) {
+ dimensions.push({ text: 'None', value: 'None' });
+ }
+
+ for (let i = 0; i < metricData.dimensions.length; i++) {
+ dimensions.push({
+ text: metricData.dimensions[i].localizedValue,
+ value: metricData.dimensions[i].value,
+ });
+ }
+ return dimensions;
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts
new file mode 100644
index 00000000000..91a51a37f4a
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/supported_namespaces.ts
@@ -0,0 +1,241 @@
+export default class SupportedNamespaces {
+ supportedMetricNamespaces = {
+ azuremonitor: [
+ 'Microsoft.AnalysisServices/servers',
+ 'Microsoft.ApiManagement/service',
+ 'Microsoft.Automation/automationAccounts',
+ 'Microsoft.Batch/batchAccounts',
+ 'Microsoft.Cache/redis',
+ 'Microsoft.ClassicCompute/virtualMachines',
+ 'Microsoft.ClassicCompute/domainNames/slots/roles',
+ 'Microsoft.CognitiveServices/accounts',
+ 'Microsoft.Compute/virtualMachines',
+ 'Microsoft.Compute/virtualMachineScaleSets',
+ 'Microsoft.ContainerInstance/containerGroups',
+ 'Microsoft.ContainerRegistry/registries',
+ 'Microsoft.ContainerService/managedClusters',
+ 'Microsoft.CustomerInsights/hubs',
+ 'Microsoft.DataBoxEdge/dataBoxEdgeDevices',
+ 'Microsoft.DataFactory/datafactories',
+ 'Microsoft.DataFactory/factories',
+ 'Microsoft.DataLakeAnalytics/accounts',
+ 'Microsoft.DataLakeStore/accounts',
+ 'Microsoft.DBforMariaDB/servers',
+ 'Microsoft.DBforMySQL/servers',
+ 'Microsoft.DBforPostgreSQL/servers',
+ 'Microsoft.Devices/IotHubs',
+ 'Microsoft.Devices/provisioningServices',
+ 'Microsoft.DocumentDB/databaseAccounts',
+ 'Microsoft.EventGrid/topics',
+ 'Microsoft.EventGrid/eventSubscriptions',
+ 'Microsoft.EventGrid/extensionTopics',
+ 'Microsoft.EventHub/namespaces',
+ 'Microsoft.EventHub/clusters',
+ 'Microsoft.HDInsight/clusters',
+ 'Microsoft.Insights/AutoscaleSettings',
+ 'Microsoft.Insights/components',
+ 'Microsoft.KeyVault/vaults',
+ 'Microsoft.Kusto/clusters',
+ 'Microsoft.LocationBasedServices/accounts',
+ 'Microsoft.Logic/workflows',
+ 'Microsoft.Logic/integrationServiceEnvironments',
+ 'Microsoft.NetApp/netAppAccounts/capacityPools',
+ 'Microsoft.NetApp/netAppAccounts/capacityPools/Volumes',
+ 'Microsoft.Network/networkInterfaces',
+ 'Microsoft.Network/loadBalancers',
+ 'Microsoft.Network/dnsZones',
+ 'Microsoft.Network/publicIPAddresses',
+ 'Microsoft.Network/azureFirewalls',
+ 'Microsoft.Network/applicationGateways',
+ 'Microsoft.Network/virtualNetworkGateways',
+ 'Microsoft.Network/expressRouteCircuits',
+ 'Microsoft.Network/expressRouteCircuits/Peerings',
+ 'Microsoft.Network/connections',
+ 'Microsoft.Network/trafficManagerProfiles',
+ 'Microsoft.Network/networkWatchers/connectionMonitors',
+ 'Microsoft.Network/frontdoors',
+ 'Microsoft.NotificationHubs/namespaces/notificationHubs',
+ 'Microsoft.PowerBIDedicated/capacities',
+ 'Microsoft.Relay/namespaces',
+ 'Microsoft.Search/searchServices',
+ 'Microsoft.ServiceBus/namespaces',
+ 'Microsoft.Sql/servers/databases',
+ 'Microsoft.Sql/servers/elasticPools',
+ 'Microsoft.Sql/managedInstances',
+ 'Microsoft.Storage/storageAccounts',
+ 'Microsoft.Storage/storageAccounts/blobServices',
+ 'Microsoft.Storage/storageAccounts/fileServices',
+ 'Microsoft.Storage/storageAccounts/queueServices',
+ 'Microsoft.Storage/storageAccounts/tableServices',
+ 'Microsoft.StorageSync/storageSyncServices',
+ 'Microsoft.StorageSync/storageSyncServices/syncGroups',
+ 'Microsoft.StorageSync/storageSyncServices/syncGroups/serverEndpoints',
+ 'Microsoft.StorageSync/storageSyncServices/registeredServers',
+ 'Microsoft.StreamAnalytics/streamingJobs',
+ 'Microsoft.Web/serverfarms',
+ 'Microsoft.Web/sites',
+ 'Microsoft.Web/sites/slots',
+ 'Microsoft.Web/hostingEnvironments/multiRolePools',
+ 'Microsoft.Web/hostingEnvironments/workerPools',
+ ],
+ govazuremonitor: [
+ 'Microsoft.AnalysisServices/servers',
+ 'Microsoft.ApiManagement/service',
+ 'Microsoft.Batch/batchAccounts',
+ 'Microsoft.Cache/redis',
+ 'Microsoft.ClassicCompute/virtualMachines',
+ 'Microsoft.ClassicCompute/domainNames/slots/roles',
+ 'Microsoft.CognitiveServices/accounts',
+ 'Microsoft.Compute/virtualMachines',
+ 'Microsoft.Compute/virtualMachineScaleSets',
+ 'Microsoft.ContainerRegistry/registries',
+ 'Microsoft.DBforMySQL/servers',
+ 'Microsoft.DBforPostgreSQL/servers',
+ 'Microsoft.Devices/IotHubs',
+ 'Microsoft.Devices/provisioningServices',
+ 'Microsoft.EventGrid/topics',
+ 'Microsoft.EventGrid/eventSubscriptions',
+ 'Microsoft.EventGrid/extensionTopics',
+ 'Microsoft.EventHub/namespaces',
+ 'Microsoft.EventHub/clusters',
+ 'Microsoft.Insights/AutoscaleSettings',
+ 'Microsoft.KeyVault/vaults',
+ 'Microsoft.Logic/workflows',
+ 'Microsoft.Network/networkInterfaces',
+ 'Microsoft.Network/loadBalancers',
+ 'Microsoft.Network/dnsZones',
+ 'Microsoft.Network/publicIPAddresses',
+ 'Microsoft.Network/azureFirewalls',
+ 'Microsoft.Network/applicationGateways',
+ 'Microsoft.Network/virtualNetworkGateways',
+ 'Microsoft.Network/expressRouteCircuits',
+ 'Microsoft.Network/expressRouteCircuits/Peerings',
+ 'Microsoft.Network/connections',
+ 'Microsoft.Network/trafficManagerProfiles',
+ 'Microsoft.Network/networkWatchers/connectionMonitors',
+ 'Microsoft.Network/frontdoors',
+ 'Microsoft.NotificationHubs/namespaces/notificationHubs',
+ 'Microsoft.OperationalInsights/workspaces',
+ 'Microsoft.PowerBIDedicated/capacities',
+ 'Microsoft.Relay/namespaces',
+ 'Microsoft.ServiceBus/namespaces',
+ 'Microsoft.Sql/servers/databases',
+ 'Microsoft.Sql/servers/elasticPools',
+ 'Microsoft.Sql/managedInstances',
+ 'Microsoft.Storage/storageAccounts',
+ 'Microsoft.Storage/storageAccounts/blobServices',
+ 'Microsoft.Storage/storageAccounts/fileServices',
+ 'Microsoft.Storage/storageAccounts/queueServices',
+ 'Microsoft.Storage/storageAccounts/tableServices',
+ 'Microsoft.Web/serverfarms',
+ 'Microsoft.Web/sites',
+ 'Microsoft.Web/sites/slots',
+ 'Microsoft.Web/hostingEnvironments/multiRolePools',
+ 'Microsoft.Web/hostingEnvironments/workerPools',
+ ],
+ germanyazuremonitor: [
+ 'Microsoft.AnalysisServices/servers',
+ 'Microsoft.Batch/batchAccounts',
+ 'Microsoft.Cache/redis',
+ 'Microsoft.ClassicCompute/virtualMachines',
+ 'Microsoft.ClassicCompute/domainNames/slots/roles',
+ 'Microsoft.Compute/virtualMachines',
+ 'Microsoft.Compute/virtualMachineScaleSets',
+ 'Microsoft.DBforMySQL/servers',
+ 'Microsoft.DBforPostgreSQL/servers',
+ 'Microsoft.Devices/IotHubs',
+ 'Microsoft.Devices/provisioningServices',
+ 'Microsoft.EventHub/namespaces',
+ 'Microsoft.EventHub/clusters',
+ 'Microsoft.Insights/AutoscaleSettings',
+ 'Microsoft.KeyVault/vaults',
+ 'Microsoft.Network/networkInterfaces',
+ 'Microsoft.Network/loadBalancers',
+ 'Microsoft.Network/dnsZones',
+ 'Microsoft.Network/publicIPAddresses',
+ 'Microsoft.Network/azureFirewalls',
+ 'Microsoft.Network/applicationGateways',
+ 'Microsoft.Network/virtualNetworkGateways',
+ 'Microsoft.Network/expressRouteCircuits',
+ 'Microsoft.Network/expressRouteCircuits/Peerings',
+ 'Microsoft.Network/connections',
+ 'Microsoft.Network/trafficManagerProfiles',
+ 'Microsoft.Network/networkWatchers/connectionMonitors',
+ 'Microsoft.Network/frontdoors',
+ 'Microsoft.NotificationHubs/namespaces/notificationHubs',
+ 'Microsoft.PowerBIDedicated/capacities',
+ 'Microsoft.Relay/namespaces',
+ 'Microsoft.ServiceBus/namespaces',
+ 'Microsoft.Sql/servers/databases',
+ 'Microsoft.Sql/servers/elasticPools',
+ 'Microsoft.Sql/managedInstances',
+ 'Microsoft.Storage/storageAccounts',
+ 'Microsoft.Storage/storageAccounts/blobServices',
+ 'Microsoft.Storage/storageAccounts/fileServices',
+ 'Microsoft.Storage/storageAccounts/queueServices',
+ 'Microsoft.Storage/storageAccounts/tableServices',
+ 'Microsoft.StreamAnalytics/streamingJobs',
+ 'Microsoft.Web/serverfarms',
+ 'Microsoft.Web/sites',
+ 'Microsoft.Web/sites/slots',
+ 'Microsoft.Web/hostingEnvironments/multiRolePools',
+ 'Microsoft.Web/hostingEnvironments/workerPools',
+ ],
+ chinaazuremonitor: [
+ 'Microsoft.AnalysisServices/servers',
+ 'Microsoft.Batch/batchAccounts',
+ 'Microsoft.Cache/redis',
+ 'Microsoft.ClassicCompute/virtualMachines',
+ 'Microsoft.ClassicCompute/domainNames/slots/roles',
+ 'Microsoft.CognitiveServices/accounts',
+ 'Microsoft.Compute/virtualMachines',
+ 'Microsoft.Compute/virtualMachineScaleSets',
+ 'Microsoft.ContainerRegistry/registries',
+ 'Microsoft.DBforMySQL/servers',
+ 'Microsoft.DBforPostgreSQL/servers',
+ 'Microsoft.Devices/IotHubs',
+ 'Microsoft.Devices/provisioningServices',
+ 'Microsoft.EventHub/namespaces',
+ 'Microsoft.Insights/AutoscaleSettings',
+ 'Microsoft.KeyVault/vaults',
+ 'Microsoft.Logic/workflows',
+ 'Microsoft.Network/networkInterfaces',
+ 'Microsoft.Network/loadBalancers',
+ 'Microsoft.Network/dnsZones',
+ 'Microsoft.Network/publicIPAddresses',
+ 'Microsoft.Network/azureFirewalls',
+ 'Microsoft.Network/applicationGateways',
+ 'Microsoft.Network/virtualNetworkGateways',
+ 'Microsoft.Network/expressRouteCircuits',
+ 'Microsoft.Network/expressRouteCircuits/Peerings',
+ 'Microsoft.Network/connections',
+ 'Microsoft.Network/trafficManagerProfiles',
+ 'Microsoft.Network/networkWatchers/connectionMonitors',
+ 'Microsoft.Network/frontdoors',
+ 'Microsoft.NotificationHubs/namespaces/notificationHubs',
+ 'Microsoft.PowerBIDedicated/capacities',
+ 'Microsoft.Relay/namespaces',
+ 'Microsoft.ServiceBus/namespaces',
+ 'Microsoft.Sql/servers/databases',
+ 'Microsoft.Sql/servers/elasticPools',
+ 'Microsoft.Sql/managedInstances',
+ 'Microsoft.Storage/storageAccounts',
+ 'Microsoft.Storage/storageAccounts/blobServices',
+ 'Microsoft.Storage/storageAccounts/fileServices',
+ 'Microsoft.Storage/storageAccounts/queueServices',
+ 'Microsoft.Storage/storageAccounts/tableServices',
+ 'Microsoft.StreamAnalytics/streamingJobs',
+ 'Microsoft.Web/serverfarms',
+ 'Microsoft.Web/sites',
+ 'Microsoft.Web/sites/slots',
+ 'Microsoft.Web/hostingEnvironments/multiRolePools',
+ 'Microsoft.Web/hostingEnvironments/workerPools',
+ ],
+ };
+
+ constructor(private cloudName: string) {}
+
+ get() {
+ return this.supportedMetricNamespaces[this.cloudName];
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts
new file mode 100644
index 00000000000..a0f5a654813
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.test.ts
@@ -0,0 +1,167 @@
+import UrlBuilder from './url_builder';
+
+describe('AzureMonitorUrlBuilder', () => {
+ describe('when metric definition is Microsoft.Sql/servers/databases', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'rg',
+ 'Microsoft.Sql/servers/databases',
+ 'rn1/rn2',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Sql/servers', () => {
+ it('should build the getMetricNames url in the shorter format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'rg',
+ 'Microsoft.Sql/servers',
+ 'rn',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Sql/servers/rn/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/blobServices',
+ 'rn1/default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices', () => {
+ it('should build the query url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorQueryUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/blobServices',
+ 'rn1/default',
+ '2017-05-01-preview',
+ 'metricnames=aMetric'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
+ 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/fileServices',
+ 'rn1/default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices', () => {
+ it('should build the query url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorQueryUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/fileServices',
+ 'rn1/default',
+ '2017-05-01-preview',
+ 'metricnames=aMetric'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
+ 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/tableServices',
+ 'rn1/default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices', () => {
+ it('should build the query url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorQueryUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/tableServices',
+ 'rn1/default',
+ '2017-05-01-preview',
+ 'metricnames=aMetric'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
+ 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices', () => {
+ it('should build the getMetricNames url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/queueServices',
+ 'rn1/default',
+ '2017-05-01-preview'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
+ 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview'
+ );
+ });
+ });
+
+ describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices', () => {
+ it('should build the query url in the longer format', () => {
+ const url = UrlBuilder.buildAzureMonitorQueryUrl(
+ '',
+ 'rg',
+ 'Microsoft.Storage/storageAccounts/queueServices',
+ 'rn1/default',
+ '2017-05-01-preview',
+ 'metricnames=aMetric'
+ );
+ expect(url).toBe(
+ '/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
+ 'providers/microsoft.insights/metrics?api-version=2017-05-01-preview&metricnames=aMetric'
+ );
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts
new file mode 100644
index 00000000000..47937076428
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/azure_monitor/url_builder.ts
@@ -0,0 +1,48 @@
+export default class UrlBuilder {
+ static buildAzureMonitorQueryUrl(
+ baseUrl: string,
+ resourceGroup: string,
+ metricDefinition: string,
+ resourceName: string,
+ apiVersion: string,
+ filter: string
+ ) {
+ if ((metricDefinition.match(/\//g) || []).length > 1) {
+ const rn = resourceName.split('/');
+ const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
+ const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
+ return (
+ `${baseUrl}/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
+ `/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}`
+ );
+ }
+
+ return (
+ `${baseUrl}/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
+ `/providers/microsoft.insights/metrics?api-version=${apiVersion}&${filter}`
+ );
+ }
+
+ static buildAzureMonitorGetMetricNamesUrl(
+ baseUrl: string,
+ resourceGroup: string,
+ metricDefinition: string,
+ resourceName: string,
+ apiVersion: string
+ ) {
+ if ((metricDefinition.match(/\//g) || []).length > 1) {
+ const rn = resourceName.split('/');
+ const service = metricDefinition.substring(metricDefinition.lastIndexOf('/') + 1);
+ const md = metricDefinition.substring(0, metricDefinition.lastIndexOf('/'));
+ return (
+ `${baseUrl}/${resourceGroup}/providers/${md}/${rn[0]}/${service}/${rn[1]}` +
+ `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}`
+ );
+ }
+
+ return (
+ `${baseUrl}/${resourceGroup}/providers/${metricDefinition}/${resourceName}` +
+ `/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}`
+ );
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts
new file mode 100644
index 00000000000..98fe5a87a56
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts
@@ -0,0 +1,46 @@
+import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
+import config from 'app/core/config';
+import { isVersionGtOrEq } from './version';
+
+export class AzureMonitorConfigCtrl {
+ static templateUrl = 'public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html';
+ current: any;
+ azureLogAnalyticsDatasource: any;
+ workspaces: any[];
+ hasRequiredGrafanaVersion: boolean;
+
+ /** @ngInject */
+ constructor($scope, backendSrv, $q) {
+ this.hasRequiredGrafanaVersion = this.hasMinVersion();
+ this.current.jsonData.cloudName = this.current.jsonData.cloudName || 'azuremonitor';
+ this.current.jsonData.azureLogAnalyticsSameAs = this.current.jsonData.azureLogAnalyticsSameAs || false;
+
+ if (this.current.id) {
+ this.current.url = '/api/datasources/proxy/' + this.current.id;
+ this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(this.current, backendSrv, null, $q);
+ this.getWorkspaces();
+ }
+ }
+
+ hasMinVersion(): boolean {
+ return isVersionGtOrEq(config.buildInfo.version, '5.2');
+ }
+
+ showMinVersionWarning() {
+ return !this.hasRequiredGrafanaVersion && this.current.secureJsonFields.logAnalyticsClientSecret;
+ }
+
+ getWorkspaces() {
+ if (!this.azureLogAnalyticsDatasource.isConfigured()) {
+ return;
+ }
+
+ return this.azureLogAnalyticsDatasource.getWorkspaces().then(workspaces => {
+ this.workspaces = workspaces;
+ if (this.workspaces.length > 0) {
+ this.current.jsonData.logAnalyticsDefaultWorkspace =
+ this.current.jsonData.logAnalyticsDefaultWorkspace || this.workspaces[0].value;
+ }
+ });
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/css/query_editor.css b/public/app/plugins/datasource/grafana-azure-monitor-datasource/css/query_editor.css
new file mode 100644
index 00000000000..305c9eadb09
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/css/query_editor.css
@@ -0,0 +1,31 @@
+.min-width-10 {
+ min-width: 10rem;
+}
+
+.min-width-12 {
+ min-width: 12rem;
+}
+
+.min-width-20 {
+ min-width: 20rem;
+}
+
+.gf-form-select-wrapper select.gf-form-input {
+ height: 2.64rem;
+}
+
+.gf-form-select-wrapper--caret-indent.gf-form-select-wrapper::after {
+ right: 0.775rem
+}
+
+.service-dropdown {
+ width: 12rem;
+}
+
+.aggregation-dropdown-wrapper {
+ max-width: 29.1rem;
+}
+
+.timegrainunit-dropdown-wrapper {
+ width: 8rem;
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts
new file mode 100644
index 00000000000..c4155cf1cd7
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/datasource.ts
@@ -0,0 +1,185 @@
+import _ from 'lodash';
+import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
+import AppInsightsDatasource from './app_insights/app_insights_datasource';
+import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
+
+export default class Datasource {
+ id: number;
+ name: string;
+ azureMonitorDatasource: AzureMonitorDatasource;
+ appInsightsDatasource: AppInsightsDatasource;
+ azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
+
+ /** @ngInject */
+ constructor(instanceSettings, private backendSrv, private templateSrv, private $q) {
+ this.name = instanceSettings.name;
+ this.id = instanceSettings.id;
+ this.azureMonitorDatasource = new AzureMonitorDatasource(
+ instanceSettings,
+ this.backendSrv,
+ this.templateSrv,
+ this.$q
+ );
+ this.appInsightsDatasource = new AppInsightsDatasource(
+ instanceSettings,
+ this.backendSrv,
+ this.templateSrv,
+ this.$q
+ );
+
+ this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(
+ instanceSettings,
+ this.backendSrv,
+ this.templateSrv,
+ this.$q
+ );
+ }
+
+ query(options) {
+ const promises: any[] = [];
+ const azureMonitorOptions = _.cloneDeep(options);
+ const appInsightsTargets = _.cloneDeep(options);
+ const azureLogAnalyticsTargets = _.cloneDeep(options);
+
+ azureMonitorOptions.targets = _.filter(azureMonitorOptions.targets, ['queryType', 'Azure Monitor']);
+ appInsightsTargets.targets = _.filter(appInsightsTargets.targets, ['queryType', 'Application Insights']);
+ azureLogAnalyticsTargets.targets = _.filter(azureLogAnalyticsTargets.targets, ['queryType', 'Azure Log Analytics']);
+
+ if (azureMonitorOptions.targets.length > 0) {
+ const amPromise = this.azureMonitorDatasource.query(azureMonitorOptions);
+ if (amPromise) {
+ promises.push(amPromise);
+ }
+ }
+
+ if (appInsightsTargets.targets.length > 0) {
+ const aiPromise = this.appInsightsDatasource.query(appInsightsTargets);
+ if (aiPromise) {
+ promises.push(aiPromise);
+ }
+ }
+
+ if (azureLogAnalyticsTargets.targets.length > 0) {
+ const alaPromise = this.azureLogAnalyticsDatasource.query(azureLogAnalyticsTargets);
+ if (alaPromise) {
+ promises.push(alaPromise);
+ }
+ }
+
+ if (promises.length === 0) {
+ return this.$q.when({ data: [] });
+ }
+
+ return Promise.all(promises).then(results => {
+ return { data: _.flatten(results) };
+ });
+ }
+
+ annotationQuery(options) {
+ return this.azureLogAnalyticsDatasource.annotationQuery(options);
+ }
+
+ metricFindQuery(query: string) {
+ if (!query) {
+ return Promise.resolve([]);
+ }
+
+ const aiResult = this.appInsightsDatasource.metricFindQuery(query);
+ if (aiResult) {
+ return aiResult;
+ }
+
+ const amResult = this.azureMonitorDatasource.metricFindQuery(query);
+ if (amResult) {
+ return amResult;
+ }
+
+ const alaResult = this.azureLogAnalyticsDatasource.metricFindQuery(query);
+ if (alaResult) {
+ return alaResult;
+ }
+
+ return Promise.resolve([]);
+ }
+
+ testDatasource() {
+ const promises: any[] = [];
+
+ if (this.azureMonitorDatasource.isConfigured()) {
+ promises.push(this.azureMonitorDatasource.testDatasource());
+ }
+
+ if (this.appInsightsDatasource.isConfigured()) {
+ promises.push(this.appInsightsDatasource.testDatasource());
+ }
+
+ if (this.azureLogAnalyticsDatasource.isConfigured()) {
+ promises.push(this.azureLogAnalyticsDatasource.testDatasource());
+ }
+
+ if (promises.length === 0) {
+ return {
+ status: 'error',
+ message: `Nothing configured. At least one of the API's must be configured.`,
+ title: 'Error',
+ };
+ }
+
+ return this.$q.all(promises).then(results => {
+ let status = 'success';
+ let message = '';
+
+ for (let i = 0; i < results.length; i++) {
+ if (results[i].status !== 'success') {
+ status = results[i].status;
+ }
+ message += `${i + 1}. ${results[i].message} `;
+ }
+
+ return {
+ status: status,
+ message: message,
+ title: _.upperFirst(status),
+ };
+ });
+ }
+
+ /* Azure Monitor REST API methods */
+ getResourceGroups() {
+ return this.azureMonitorDatasource.getResourceGroups();
+ }
+
+ getMetricDefinitions(resourceGroup: string) {
+ return this.azureMonitorDatasource.getMetricDefinitions(resourceGroup);
+ }
+
+ getResourceNames(resourceGroup: string, metricDefinition: string) {
+ return this.azureMonitorDatasource.getResourceNames(resourceGroup, metricDefinition);
+ }
+
+ getMetricNames(resourceGroup: string, metricDefinition: string, resourceName: string) {
+ return this.azureMonitorDatasource.getMetricNames(resourceGroup, metricDefinition, resourceName);
+ }
+
+ getMetricMetadata(resourceGroup: string, metricDefinition: string, resourceName: string, metricName: string) {
+ return this.azureMonitorDatasource.getMetricMetadata(resourceGroup, metricDefinition, resourceName, metricName);
+ }
+
+ /* Application Insights API method */
+ getAppInsightsMetricNames() {
+ return this.appInsightsDatasource.getMetricNames();
+ }
+
+ getAppInsightsMetricMetadata(metricName) {
+ return this.appInsightsDatasource.getMetricMetadata(metricName);
+ }
+
+ getAppInsightsColumns(refId) {
+ return this.appInsightsDatasource.logAnalyticsColumns[refId];
+ }
+
+ /*Azure Log Analytics */
+ getAzureLogAnalyticsWorkspaces() {
+ return this.azureLogAnalyticsDatasource.getWorkspaces();
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx
new file mode 100644
index 00000000000..849cf62efe0
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx
@@ -0,0 +1,312 @@
+import Plain from 'slate-plain-serializer';
+
+import QueryField from './query_field';
+// import debounce from './utils/debounce';
+// import {getNextCharacter} from './utils/dom';
+import debounce from 'app/features/explore/utils/debounce';
+import { getNextCharacter } from 'app/features/explore/utils/dom';
+
+import { FUNCTIONS, KEYWORDS } from './kusto';
+// import '../sass/editor.base.scss';
+
+
+const TYPEAHEAD_DELAY = 500;
+
+interface Suggestion {
+ text: string;
+ deleteBackwards?: number;
+ type?: string;
+}
+
+interface SuggestionGroup {
+ label: string;
+ items: Suggestion[];
+ prefixMatch?: boolean;
+ skipFilter?: boolean;
+}
+
+const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
+const wrapText = text => ({ text });
+
+export default class KustoQueryField extends QueryField {
+ fields: any;
+ events: any;
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY);
+ }
+
+ componentDidMount() {
+ this.updateMenu();
+ }
+
+ onTypeahead = () => {
+ const selection = window.getSelection();
+ if (selection.anchorNode) {
+ const wrapperNode = selection.anchorNode.parentElement;
+ if (wrapperNode === null) {
+ return;
+ }
+ const editorNode = wrapperNode.closest('.slate-query-field');
+ if (!editorNode || this.state.value.isBlurred) {
+ // Not inside this editor
+ return;
+ }
+
+ // DOM ranges
+ const range = selection.getRangeAt(0);
+ const text = selection.anchorNode.textContent;
+ if (text === null) {
+ return;
+ }
+ const offset = range.startOffset;
+ let prefix = cleanText(text.substr(0, offset));
+
+ // Model ranges
+ const modelOffset = this.state.value.anchorOffset;
+ const modelPrefix = this.state.value.anchorText.text.slice(0, modelOffset);
+
+ // Determine candidates by context
+ let suggestionGroups: SuggestionGroup[] = [];
+ const wrapperClasses = wrapperNode.classList;
+ let typeaheadContext: string | null = null;
+
+ if (wrapperClasses.contains('function-context')) {
+ typeaheadContext = 'context-function';
+ if (this.fields) {
+ suggestionGroups = this._getKeywordSuggestions();
+ } else {
+ this._fetchFields();
+ return;
+ }
+ } else if (modelPrefix.match(/(facet\s$)/i)) {
+ typeaheadContext = 'context-facet';
+ if (this.fields) {
+ suggestionGroups = this._getKeywordSuggestions();
+ } else {
+ this._fetchFields();
+ return;
+ }
+ } else if (modelPrefix.match(/(,\s*$)/)) {
+ typeaheadContext = 'context-multiple-fields';
+ if (this.fields) {
+ suggestionGroups = this._getKeywordSuggestions();
+ } else {
+ this._fetchFields();
+ return;
+ }
+ } else if (modelPrefix.match(/(from\s$)/i)) {
+ typeaheadContext = 'context-from';
+ if (this.events) {
+ suggestionGroups = this._getKeywordSuggestions();
+ } else {
+ this._fetchEvents();
+ return;
+ }
+ } else if (modelPrefix.match(/(^select\s\w*$)/i)) {
+ typeaheadContext = 'context-select';
+ if (this.fields) {
+ suggestionGroups = this._getKeywordSuggestions();
+ } else {
+ this._fetchFields();
+ return;
+ }
+ } else if (modelPrefix.match(/from\s\S+\s\w*$/i)) {
+ prefix = '';
+ typeaheadContext = 'context-since';
+ suggestionGroups = this._getKeywordSuggestions();
+ // } else if (modelPrefix.match(/\d+\s\w*$/)) {
+ // typeaheadContext = 'context-number';
+ // suggestionGroups = this._getAfterNumberSuggestions();
+ } else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) {
+ typeaheadContext = 'context-timeseries';
+ suggestionGroups = this._getKeywordSuggestions();
+ } else if (prefix && !wrapperClasses.contains('argument')) {
+ typeaheadContext = 'context-builtin';
+ suggestionGroups = this._getKeywordSuggestions();
+ } else if (Plain.serialize(this.state.value) === '') {
+ typeaheadContext = 'context-new';
+ suggestionGroups = this._getInitialSuggestions();
+ }
+
+ let results = 0;
+ prefix = prefix.toLowerCase();
+ const filteredSuggestions = suggestionGroups.map(group => {
+ if (group.items && prefix && !group.skipFilter) {
+ group.items = group.items.filter(c => c.text.length >= prefix.length);
+ if (group.prefixMatch) {
+ group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) === 0);
+ } else {
+ group.items = group.items.filter(c => c.text.toLowerCase().indexOf(prefix) > -1);
+ }
+ }
+ results += group.items.length;
+ return group;
+ })
+ .filter(group => group.items.length > 0);
+
+ // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext);
+
+ this.setState({
+ typeaheadPrefix: prefix,
+ typeaheadContext,
+ typeaheadText: text,
+ suggestions: results > 0 ? filteredSuggestions : [],
+ });
+ }
+ }
+
+ applyTypeahead(change, suggestion) {
+ const { typeaheadPrefix, typeaheadContext, typeaheadText } = this.state;
+ let suggestionText = suggestion.text || suggestion;
+ const move = 0;
+
+ // Modify suggestion based on context
+
+ const nextChar = getNextCharacter();
+ if (suggestion.type === 'function') {
+ if (!nextChar || nextChar !== '(') {
+ suggestionText += '(';
+ }
+ } else if (typeaheadContext === 'context-function') {
+ if (!nextChar || nextChar !== ')') {
+ suggestionText += ')';
+ }
+ } else {
+ if (!nextChar || nextChar !== ' ') {
+ suggestionText += ' ';
+ }
+ }
+
+ this.resetTypeahead();
+
+ // Remove the current, incomplete text and replace it with the selected suggestion
+ const backward = suggestion.deleteBackwards || typeaheadPrefix.length;
+ const text = cleanText(typeaheadText);
+ const suffixLength = text.length - typeaheadPrefix.length;
+ const offset = typeaheadText.indexOf(typeaheadPrefix);
+ const midWord = typeaheadPrefix && ((suffixLength > 0 && offset > -1) || suggestionText === typeaheadText);
+ const forward = midWord ? suffixLength + offset : 0;
+
+ return change
+ .deleteBackward(backward)
+ .deleteForward(forward)
+ .insertText(suggestionText)
+ .move(move)
+ .focus();
+ }
+
+ // private _getFieldsSuggestions(): SuggestionGroup[] {
+ // return [
+ // {
+ // prefixMatch: true,
+ // label: 'Fields',
+ // items: this.fields.map(wrapText)
+ // },
+ // {
+ // prefixMatch: true,
+ // label: 'Variables',
+ // items: this.props.templateVariables.map(wrapText)
+ // }
+ // ];
+ // }
+
+ // private _getAfterFromSuggestions(): SuggestionGroup[] {
+ // return [
+ // {
+ // skipFilter: true,
+ // label: 'Events',
+ // items: this.events.map(wrapText)
+ // },
+ // {
+ // prefixMatch: true,
+ // label: 'Variables',
+ // items: this.props.templateVariables
+ // .map(wrapText)
+ // .map(suggestion => {
+ // suggestion.deleteBackwards = 0;
+ // return suggestion;
+ // })
+ // }
+ // ];
+ // }
+
+ // private _getAfterSelectSuggestions(): SuggestionGroup[] {
+ // return [
+ // {
+ // prefixMatch: true,
+ // label: 'Fields',
+ // items: this.fields.map(wrapText)
+ // },
+ // {
+ // prefixMatch: true,
+ // label: 'Functions',
+ // items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; })
+ // },
+ // {
+ // prefixMatch: true,
+ // label: 'Variables',
+ // items: this.props.templateVariables.map(wrapText)
+ // }
+ // ];
+ // }
+
+ private _getKeywordSuggestions(): SuggestionGroup[] {
+ return [
+ {
+ prefixMatch: true,
+ label: 'Keywords',
+ items: KEYWORDS.map(wrapText)
+ },
+ {
+ prefixMatch: true,
+ label: 'Functions',
+ items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; })
+ }
+ ];
+ }
+
+ private _getInitialSuggestions(): SuggestionGroup[] {
+ // TODO: return datbase tables as an initial suggestion
+ return [
+ {
+ prefixMatch: true,
+ label: 'Keywords',
+ items: KEYWORDS.map(wrapText)
+ },
+ {
+ prefixMatch: true,
+ label: 'Functions',
+ items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; })
+ }
+ ];
+ }
+
+ private async _fetchEvents() {
+ // const query = 'events';
+ // const result = await this.request(query);
+
+ // if (result === undefined) {
+ // this.events = [];
+ // } else {
+ // this.events = result;
+ // }
+ // setTimeout(this.onTypeahead, 0);
+
+ //Stub
+ this.events = [];
+ }
+
+ private async _fetchFields() {
+ // const query = 'fields';
+ // const result = await this.request(query);
+
+ // this.fields = result || [];
+
+ // setTimeout(this.onTypeahead, 0);
+ // Stub
+ this.fields = [];
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx
new file mode 100644
index 00000000000..da7db58567f
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx
@@ -0,0 +1,59 @@
+import KustoQueryField from './KustoQueryField';
+import Kusto from './kusto';
+
+import React, { Component } from 'react';
+import coreModule from 'app/core/core_module';
+
+class Editor extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ edited: false,
+ query: props.query || '',
+ };
+ }
+
+ onChangeQuery = value => {
+ const { index, change } = this.props;
+ const { query } = this.state;
+ const edited = query !== value;
+ this.setState({ edited, query: value });
+ if (change) {
+ change(value, index);
+ }
+ };
+
+ onPressEnter = () => {
+ const { execute } = this.props;
+ if (execute) {
+ execute();
+ }
+ };
+
+ render() {
+ const { request, variables } = this.props;
+ const { edited, query } = this.state;
+
+ return (
+
+
+
+ );
+ }
+}
+
+coreModule.directive('kustoEditor', [
+ 'reactDirective',
+ reactDirective => {
+ return reactDirective(Editor, ['change', 'database', 'execute', 'query', 'request', 'variables']);
+ },
+]);
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts
new file mode 100644
index 00000000000..647ebb8024a
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts
@@ -0,0 +1,114 @@
+export const FUNCTIONS = [
+ { text: 'countof', display: 'countof()', hint: '' },
+ { text: 'bin', display: 'bin()', hint: '' },
+ { text: 'extentid', display: 'extentid()', hint: '' },
+ { text: 'extract', display: 'extract()', hint: '' },
+ { text: 'extractjson', display: 'extractjson()', hint: '' },
+ { text: 'floor', display: 'floor()', hint: '' },
+ { text: 'iif', display: 'iif()', hint: '' },
+ { text: 'isnull', display: 'isnull()', hint: '' },
+ { text: 'isnotnull', display: 'isnotnull()', hint: '' },
+ { text: 'notnull', display: 'notnull()', hint: '' },
+ { text: 'isempty', display: 'isempty()', hint: '' },
+ { text: 'isnotempty', display: 'isnotempty()', hint: '' },
+ { text: 'notempty', display: 'notempty()', hint: '' },
+ { text: 'now', display: 'now()', hint: '' },
+ { text: 're2', display: 're2()', hint: '' },
+ { text: 'strcat', display: 'strcat()', hint: '' },
+ { text: 'strlen', display: 'strlen()', hint: '' },
+ { text: 'toupper', display: 'toupper()', hint: '' },
+ { text: 'tostring', display: 'tostring()', hint: '' },
+ { text: 'count', display: 'count()', hint: '' },
+ { text: 'cnt', display: 'cnt()', hint: '' },
+ { text: 'sum', display: 'sum()', hint: '' },
+ { text: 'min', display: 'min()', hint: '' },
+ { text: 'max', display: 'max()', hint: '' },
+ { text: 'avg', display: 'avg()', hint: '' },
+ {
+ text: '$__timeFilter',
+ display: '$__timeFilter()',
+ hint: 'Macro that uses the selected timerange in Grafana to filter the query.',
+ },
+ {
+ 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.' },
+];
+
+export const KEYWORDS = [
+ 'by',
+ 'on',
+ 'contains',
+ 'notcontains',
+ 'containscs',
+ 'notcontainscs',
+ 'startswith',
+ 'has',
+ 'matches',
+ 'regex',
+ 'true',
+ 'false',
+ 'and',
+ 'or',
+ 'typeof',
+ 'int',
+ 'string',
+ 'date',
+ 'datetime',
+ 'time',
+ 'long',
+ 'real',
+ 'boolean',
+ 'bool',
+ // add some more keywords
+ 'where',
+ 'order',
+];
+
+// Kusto operators
+// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~'];
+
+export const DURATION = ['SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS'];
+
+const tokenizer = {
+ comment: {
+ pattern: /(^|[^\\:])\/\/.*/,
+ lookbehind: true,
+ greedy: true,
+ },
+ 'function-context': {
+ pattern: /[a-z0-9_]+\([^)]*\)?/i,
+ inside: {},
+ },
+ duration: {
+ pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'),
+ alias: 'number',
+ },
+ builtin: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.text).join('|')})(?=\\s*\\()`, 'i'),
+ string: {
+ pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
+ greedy: true,
+ },
+ keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|\\*)\\b`, 'i'),
+ boolean: /\b(?:true|false)\b/,
+ number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,
+ operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/,
+ punctuation: /[{};(),.:]/,
+ variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/,
+};
+
+tokenizer['function-context'].inside = {
+ argument: {
+ pattern: /[a-z0-9_]+(?=:)/i,
+ alias: 'symbol',
+ },
+ duration: tokenizer.duration,
+ number: tokenizer.number,
+ builtin: tokenizer.builtin,
+ string: tokenizer.string,
+ variable: tokenizer.variable,
+};
+
+export default tokenizer;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx
new file mode 100644
index 00000000000..1c883a40c31
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx
@@ -0,0 +1,335 @@
+import PluginPrism from './slate-plugins/prism';
+// import PluginPrism from 'slate-prism';
+// import Prism from 'prismjs';
+
+import BracesPlugin from 'app/features/explore/slate-plugins/braces';
+import ClearPlugin from 'app/features/explore/slate-plugins/clear';
+// Custom plugins (new line on Enter and run on Shift+Enter)
+import NewlinePlugin from './slate-plugins/newline';
+import RunnerPlugin from './slate-plugins/runner';
+
+import Typeahead from './typeahead';
+
+import { Block, Document, Text, Value } from 'slate';
+import { Editor } from 'slate-react';
+import Plain from 'slate-plain-serializer';
+import ReactDOM from 'react-dom';
+import React from 'react';
+import _ from 'lodash';
+
+function flattenSuggestions(s) {
+ return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
+}
+
+export const makeFragment = text => {
+ const lines = text.split('\n').map(line =>
+ Block.create({
+ type: 'paragraph',
+ nodes: [Text.create(line)],
+ })
+ );
+
+ const fragment = Document.create({
+ nodes: lines,
+ });
+ return fragment;
+};
+
+export const getInitialValue = query => Value.create({ document: makeFragment(query) });
+
+class Portal extends React.Component {
+ node: any;
+
+ constructor(props) {
+ super(props);
+ const { index = 0, prefix = 'query' } = props;
+ this.node = document.createElement('div');
+ this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`);
+ document.body.appendChild(this.node);
+ }
+
+ componentWillUnmount() {
+ document.body.removeChild(this.node);
+ }
+
+ render() {
+ return ReactDOM.createPortal(this.props.children, this.node);
+ }
+}
+
+class QueryField extends React.Component {
+ menuEl: any;
+ plugins: any;
+ resetTimer: any;
+
+ constructor(props, context) {
+ super(props, context);
+
+ const { prismDefinition = {}, prismLanguage = 'kusto' } = props;
+
+ this.plugins = [
+ BracesPlugin(),
+ ClearPlugin(),
+ RunnerPlugin({ handler: props.onPressEnter }),
+ NewlinePlugin(),
+ PluginPrism({ definition: prismDefinition, language: prismLanguage }),
+ ];
+
+ this.state = {
+ labelKeys: {},
+ labelValues: {},
+ suggestions: [],
+ typeaheadIndex: 0,
+ typeaheadPrefix: '',
+ value: getInitialValue(props.initialQuery || ''),
+ };
+ }
+
+ componentDidMount() {
+ this.updateMenu();
+ }
+
+ componentWillUnmount() {
+ clearTimeout(this.resetTimer);
+ }
+
+ componentDidUpdate() {
+ this.updateMenu();
+ }
+
+ onChange = ({ value }) => {
+ const changed = value.document !== this.state.value.document;
+ this.setState({ value }, () => {
+ if (changed) {
+ this.onChangeQuery();
+ }
+ });
+
+ window.requestAnimationFrame(this.onTypeahead);
+ };
+
+ request = (url?) => {
+ if (this.props.request) {
+ return this.props.request(url);
+ }
+ return fetch(url);
+ };
+
+ onChangeQuery = () => {
+ // Send text change to parent
+ const { onQueryChange } = this.props;
+ if (onQueryChange) {
+ onQueryChange(Plain.serialize(this.state.value));
+ }
+ };
+
+ onKeyDown = (event, change) => {
+ const { typeaheadIndex, suggestions } = this.state;
+
+ switch (event.key) {
+ case 'Escape': {
+ if (this.menuEl) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.resetTypeahead();
+ return true;
+ }
+ break;
+ }
+
+ case ' ': {
+ if (event.ctrlKey) {
+ event.preventDefault();
+ this.onTypeahead();
+ return true;
+ }
+ break;
+ }
+
+ case 'Tab':
+ case 'Enter': {
+ if (this.menuEl) {
+ // Dont blur input
+ event.preventDefault();
+ if (!suggestions || suggestions.length === 0) {
+ return undefined;
+ }
+
+ // Get the currently selected suggestion
+ const flattenedSuggestions = flattenSuggestions(suggestions);
+ const selected = Math.abs(typeaheadIndex);
+ const selectedIndex = selected % flattenedSuggestions.length || 0;
+ const suggestion = flattenedSuggestions[selectedIndex];
+
+ this.applyTypeahead(change, suggestion);
+ return true;
+ }
+ break;
+ }
+
+ case 'ArrowDown': {
+ if (this.menuEl) {
+ // Select next suggestion
+ event.preventDefault();
+ this.setState({ typeaheadIndex: typeaheadIndex + 1 });
+ }
+ break;
+ }
+
+ case 'ArrowUp': {
+ if (this.menuEl) {
+ // Select previous suggestion
+ event.preventDefault();
+ this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
+ }
+ break;
+ }
+
+ default: {
+ // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
+ break;
+ }
+ }
+ return undefined;
+ };
+
+ onTypeahead = (change?, item?) => {
+ return change || this.state.value.change();
+ };
+
+ applyTypeahead(change?, suggestion?): { value: object } {
+ return { value: {} };
+ }
+
+ resetTypeahead = () => {
+ this.setState({
+ suggestions: [],
+ typeaheadIndex: 0,
+ typeaheadPrefix: '',
+ typeaheadContext: null,
+ });
+ };
+
+ handleBlur = () => {
+ const { onBlur } = this.props;
+ // If we dont wait here, menu clicks wont work because the menu
+ // will be gone.
+ this.resetTimer = setTimeout(this.resetTypeahead, 100);
+ if (onBlur) {
+ onBlur();
+ }
+ };
+
+ handleFocus = () => {
+ const { onFocus } = this.props;
+ if (onFocus) {
+ onFocus();
+ }
+ };
+
+ onClickItem = item => {
+ const { suggestions } = this.state;
+ if (!suggestions || suggestions.length === 0) {
+ return;
+ }
+
+ // Get the currently selected suggestion
+ const flattenedSuggestions = flattenSuggestions(suggestions);
+ const suggestion = _.find(
+ flattenedSuggestions,
+ suggestion => suggestion.display === item || suggestion.text === item
+ );
+
+ // Manually triggering change
+ const change = this.applyTypeahead(this.state.value.change(), suggestion);
+ this.onChange(change);
+ };
+
+ updateMenu = () => {
+ const { suggestions } = this.state;
+ const menu = this.menuEl;
+ const selection = window.getSelection();
+ const node = selection.anchorNode;
+
+ // No menu, nothing to do
+ if (!menu) {
+ return;
+ }
+
+ // No suggestions or blur, remove menu
+ const hasSuggesstions = suggestions && suggestions.length > 0;
+ if (!hasSuggesstions) {
+ menu.removeAttribute('style');
+ return;
+ }
+
+ // Align menu overlay to editor node
+ if (node && node.parentElement) {
+ // Read from DOM
+ const rect = node.parentElement.getBoundingClientRect();
+ const scrollX = window.scrollX;
+ const scrollY = window.scrollY;
+
+ // Write DOM
+ requestAnimationFrame(() => {
+ menu.style.opacity = 1;
+ menu.style.top = `${rect.top + scrollY + rect.height + 4}px`;
+ menu.style.left = `${rect.left + scrollX - 2}px`;
+ });
+ }
+ };
+
+ menuRef = el => {
+ this.menuEl = el;
+ };
+
+ renderMenu = () => {
+ const { portalPrefix } = this.props;
+ const { suggestions } = this.state;
+ const hasSuggesstions = suggestions && suggestions.length > 0;
+ if (!hasSuggesstions) {
+ return null;
+ }
+
+ // Guard selectedIndex to be within the length of the suggestions
+ let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
+ const flattenedSuggestions = flattenSuggestions(suggestions);
+ selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
+ const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(
+ i => (typeof i === 'object' ? i.text : i)
+ );
+
+ // Create typeahead in DOM root so we can later position it absolutely
+ return (
+
+
+
+ );
+ };
+
+ render() {
+ return (
+
+ {this.renderMenu()}
+
+
+ );
+ }
+}
+
+export default QueryField;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts
new file mode 100644
index 00000000000..d484d93a542
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts
@@ -0,0 +1,35 @@
+function getIndent(text) {
+ let offset = text.length - text.trimLeft().length;
+ if (offset) {
+ let indent = text[0];
+ while (--offset) {
+ indent += text[0];
+ }
+ return indent;
+ }
+ return '';
+}
+
+export default function NewlinePlugin() {
+ return {
+ onKeyDown(event, change) {
+ const { value } = change;
+ if (!value.isCollapsed) {
+ return undefined;
+ }
+
+ if (event.key === 'Enter' && !event.shiftKey) {
+ event.preventDefault();
+
+ const { startBlock } = value;
+ const currentLineText = startBlock.text;
+ const indent = getIndent(currentLineText);
+
+ return change
+ .splitBlock()
+ .insertText(indent)
+ .focus();
+ }
+ },
+ };
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx
new file mode 100644
index 00000000000..7c1b2f86139
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx
@@ -0,0 +1,123 @@
+import React from 'react';
+import Prism from 'prismjs';
+
+const TOKEN_MARK = 'prism-token';
+
+export function setPrismTokens(language, field, values, alias = 'variable') {
+ Prism.languages[language][field] = {
+ alias,
+ pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`),
+ };
+}
+
+/**
+ * Code-highlighting plugin based on Prism and
+ * https://github.com/ianstormtaylor/slate/blob/master/examples/code-highlighting/index.js
+ *
+ * (Adapted to handle nested grammar definitions.)
+ */
+
+export default function PrismPlugin({ definition, language }) {
+ if (definition) {
+ // Don't override exising modified definitions
+ Prism.languages[language] = Prism.languages[language] || definition;
+ }
+
+ return {
+ /**
+ * Render a Slate mark with appropiate CSS class names
+ *
+ * @param {Object} props
+ * @return {Element}
+ */
+
+ renderMark(props) {
+ const { children, mark } = props;
+ // Only apply spans to marks identified by this plugin
+ if (mark.type !== TOKEN_MARK) {
+ return undefined;
+ }
+ const className = `token ${mark.data.get('types')}`;
+ return {children} ;
+ },
+
+ /**
+ * Decorate code blocks with Prism.js highlighting.
+ *
+ * @param {Node} node
+ * @return {Array}
+ */
+
+ decorateNode(node) {
+ if (node.type !== 'paragraph') {
+ return [];
+ }
+
+ const texts = node.getTexts().toArray();
+ const tstring = texts.map(t => t.text).join('\n');
+ const grammar = Prism.languages[language];
+ const tokens = Prism.tokenize(tstring, grammar);
+ const decorations: any[] = [];
+ let startText = texts.shift();
+ let endText = startText;
+ let startOffset = 0;
+ let endOffset = 0;
+ let start = 0;
+
+ function processToken(token, acc?) {
+ // Accumulate token types down the tree
+ const types = `${acc || ''} ${token.type || ''} ${token.alias || ''}`;
+
+ // Add mark for token node
+ if (typeof token === 'string' || typeof token.content === 'string') {
+ startText = endText;
+ startOffset = endOffset;
+
+ const content = typeof token === 'string' ? token : token.content;
+ const newlines = content.split('\n').length - 1;
+ const length = content.length - newlines;
+ const end = start + length;
+
+ let available = startText.text.length - startOffset;
+ let remaining = length;
+
+ endOffset = startOffset + remaining;
+
+ while (available < remaining) {
+ endText = texts.shift();
+ remaining = length - available;
+ available = endText.text.length;
+ endOffset = remaining;
+ }
+
+ // Inject marks from up the tree (acc) as well
+ if (typeof token !== 'string' || acc) {
+ const range = {
+ anchorKey: startText.key,
+ anchorOffset: startOffset,
+ focusKey: endText.key,
+ focusOffset: endOffset,
+ marks: [{ type: TOKEN_MARK, data: { types } }],
+ };
+
+ decorations.push(range);
+ }
+
+ start = end;
+ } else if (token.content && token.content.length) {
+ // Tokens can be nested
+ for (const subToken of token.content) {
+ processToken(subToken, types);
+ }
+ }
+ }
+
+ // Process top-level tokens
+ for (const token of tokens) {
+ processToken(token);
+ }
+
+ return decorations;
+ },
+ };
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts
new file mode 100644
index 00000000000..068bd9f0ad1
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts
@@ -0,0 +1,14 @@
+export default function RunnerPlugin({ handler }) {
+ return {
+ onKeyDown(event) {
+ // Handle enter
+ if (handler && event.key === 'Enter' && event.shiftKey) {
+ // Submit on Enter
+ event.preventDefault();
+ handler(event);
+ return true;
+ }
+ return undefined;
+ },
+ };
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/typeahead.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/typeahead.tsx
new file mode 100644
index 00000000000..44fce7f8c7e
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/typeahead.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+
+function scrollIntoView(el) {
+ if (!el || !el.offsetParent) {
+ return;
+ }
+ const container = el.offsetParent;
+ if (el.offsetTop > container.scrollTop + container.offsetHeight || el.offsetTop < container.scrollTop) {
+ container.scrollTop = el.offsetTop - container.offsetTop;
+ }
+}
+
+class TypeaheadItem extends React.PureComponent {
+ el: any;
+ componentDidUpdate(prevProps) {
+ if (this.props.isSelected && !prevProps.isSelected) {
+ scrollIntoView(this.el);
+ }
+ }
+
+ getRef = el => {
+ this.el = el;
+ };
+
+ render() {
+ const { hint, isSelected, label, onClickItem } = this.props;
+ const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
+ const onClick = () => onClickItem(label);
+ return (
+
+ {label}
+ {hint && isSelected ? {hint}
: null}
+
+ );
+ }
+}
+
+class TypeaheadGroup extends React.PureComponent {
+ render() {
+ const { items, label, selected, onClickItem } = this.props;
+ return (
+
+ {label}
+
+ {items.map(item => {
+ const text = typeof item === 'object' ? item.text : item;
+ const label = typeof item === 'object' ? item.display || item.text : item;
+ return (
+ -1}
+ hint={item.hint}
+ label={label}
+ />
+ );
+ })}
+
+
+ );
+ }
+}
+
+class Typeahead extends React.PureComponent {
+ render() {
+ const { groupedItems, menuRef, selectedItems, onClickItem } = this.props;
+ return (
+
+ {groupedItems.map(g => (
+
+ ))}
+
+ );
+ }
+}
+
+export default Typeahead;
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png
new file mode 100644
index 00000000000..f588a6fb6f1
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_cpu.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_network.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_network.png
new file mode 100644
index 00000000000..ea2b5a920f5
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/azure_monitor_network.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_1_select_type.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_1_select_type.png
new file mode 100644
index 00000000000..f1606245940
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_1_select_type.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_2_azure_monitor_api_details.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_2_azure_monitor_api_details.png
new file mode 100644
index 00000000000..947e58e50ab
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_2_azure_monitor_api_details.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_3_app_insights_api_details.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_3_app_insights_api_details.png
new file mode 100644
index 00000000000..3bf3f08e32a
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_3_app_insights_api_details.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_4_save_and_test.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_4_save_and_test.png
new file mode 100644
index 00000000000..ce892d8d802
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/config_4_save_and_test.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png
new file mode 100644
index 00000000000..d23e3e753a7
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/contoso_loans_grafana_dashboard.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/grafana_cloud_install.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/grafana_cloud_install.png
new file mode 100644
index 00000000000..0a51314ec6a
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/grafana_cloud_install.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/grafana_cloud_login.png b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/grafana_cloud_login.png
new file mode 100644
index 00000000000..245314effc9
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/grafana_cloud_login.png differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg
new file mode 100644
index 00000000000..d1a529d4d6e
Binary files /dev/null and b/public/app/plugins/datasource/grafana-azure-monitor-datasource/img/logo.jpg differ
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/log_analytics/querystring_builder.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/log_analytics/querystring_builder.test.ts
new file mode 100644
index 00000000000..24720edee34
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/log_analytics/querystring_builder.test.ts
@@ -0,0 +1,166 @@
+import LogAnalyticsQuerystringBuilder from './querystring_builder';
+import moment from 'moment';
+
+describe('LogAnalyticsDatasource', () => {
+ let builder: LogAnalyticsQuerystringBuilder;
+
+ beforeEach(() => {
+ builder = new LogAnalyticsQuerystringBuilder(
+ 'query=Tablename | where $__timeFilter()',
+ {
+ interval: '5m',
+ range: {
+ from: moment().subtract(24, 'hours'),
+ to: moment(),
+ },
+ rangeRaw: {
+ from: 'now-24h',
+ to: 'now',
+ },
+ },
+ 'TimeGenerated'
+ );
+ });
+
+ describe('when $__timeFilter has no column parameter', () => {
+ it('should generate a time filter condition with TimeGenerated as the datetime field', () => {
+ const query = builder.generate().uriString;
+
+ expect(query).toContain('where%20TimeGenerated%20%3E%3D%20datetime(');
+ });
+ });
+
+ describe('when $__timeFilter has a column parameter', () => {
+ beforeEach(() => {
+ builder.rawQueryString = 'query=Tablename | where $__timeFilter(myTime)';
+ });
+
+ it('should generate a time filter condition with myTime as the datetime field', () => {
+ const query = builder.generate().uriString;
+
+ expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
+ });
+ });
+
+ describe('when $__contains and multi template variable has custom All value', () => {
+ beforeEach(() => {
+ builder.rawQueryString = 'query=Tablename | where $__contains(col, all)';
+ });
+
+ it('should generate a where..in clause', () => {
+ const query = builder.generate().rawQuery;
+
+ expect(query).toContain(`where 1 == 1`);
+ });
+ });
+
+ describe('when $__contains and multi template variable has one selected value', () => {
+ beforeEach(() => {
+ builder.rawQueryString = `query=Tablename | where $__contains(col, 'val1')`;
+ });
+
+ it('should generate a where..in clause', () => {
+ const query = builder.generate().rawQuery;
+
+ expect(query).toContain(`where col in ('val1')`);
+ });
+ });
+
+ describe('when $__contains and multi template variable has multiple selected values', () => {
+ beforeEach(() => {
+ builder.rawQueryString = `query=Tablename | where $__contains(col, 'val1','val2')`;
+ });
+
+ it('should generate a where..in clause', () => {
+ const query = builder.generate().rawQuery;
+
+ expect(query).toContain(`where col in ('val1','val2')`);
+ });
+ });
+
+ describe('when $__interval is in the query', () => {
+ beforeEach(() => {
+ builder.rawQueryString = 'query=Tablename | summarize count() by Category, bin(TimeGenerated, $__interval)';
+ });
+
+ it('should replace $__interval with the inbuilt interval option', () => {
+ const query = builder.generate().uriString;
+
+ expect(query).toContain('bin(TimeGenerated%2C%205m');
+ });
+ });
+
+ describe('when using $__from and $__to is in the query and range is until now', () => {
+ beforeEach(() => {
+ builder.rawQueryString = 'query=Tablename | where myTime >= $__from and myTime <= $__to';
+ });
+
+ it('should replace $__from and $__to with a datetime and the now() function', () => {
+ const query = builder.generate().uriString;
+
+ expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
+ expect(query).toContain('myTime%20%3C%3D%20now()');
+ });
+ });
+
+ describe('when using $__from and $__to is in the query and range is a specific interval', () => {
+ beforeEach(() => {
+ builder.rawQueryString = 'query=Tablename | where myTime >= $__from and myTime <= $__to';
+ builder.options.range.to = moment().subtract(1, 'hour');
+ builder.options.rangeRaw.to = 'now-1h';
+ });
+
+ it('should replace $__from and $__to with datetimes', () => {
+ const query = builder.generate().uriString;
+
+ expect(query).toContain('where%20myTime%20%3E%3D%20datetime(');
+ expect(query).toContain('myTime%20%3C%3D%20datetime(');
+ });
+ });
+
+ describe('when using $__escape and multi template variable has one selected value', () => {
+ beforeEach(() => {
+ builder.rawQueryString = `$__escapeMulti('\\grafana-vm\Network(eth0)\Total Bytes Received')`;
+ });
+
+ it('should replace $__escape(val) with KQL style escaped string', () => {
+ const query = builder.generate().uriString;
+ expect(query).toContain(`%40'%5Cgrafana-vmNetwork(eth0)Total%20Bytes%20Received'`);
+ });
+ });
+
+ describe('when using $__escape and multi template variable has multiple selected values', () => {
+ beforeEach(() => {
+ builder.rawQueryString = `CounterPath in ($__escapeMulti('\\grafana-vm\Network(eth0)\Total','\\grafana-vm\Network(eth0)\Total'))`;
+ });
+
+ it('should replace $__escape(val) with multiple KQL style escaped string', () => {
+ const query = builder.generate().uriString;
+ expect(query).toContain(
+ `CounterPath%20in%20(%40'%5Cgrafana-vmNetwork(eth0)Total'%2C%20%40'%5Cgrafana-vmNetwork(eth0)Total')`
+ );
+ });
+ });
+
+ describe('when using $__escape and multi template variable has one selected value that contains comma', () => {
+ beforeEach(() => {
+ builder.rawQueryString = `$__escapeMulti('\\grafana-vm,\Network(eth0)\Total Bytes Received')`;
+ });
+
+ it('should replace $__escape(val) with KQL style escaped string', () => {
+ const query = builder.generate().uriString;
+ expect(query).toContain(`%40'%5Cgrafana-vm%2CNetwork(eth0)Total%20Bytes%20Received'`);
+ });
+ });
+
+ describe(`when using $__escape and multi template variable value is not wrapped in single '`, () => {
+ beforeEach(() => {
+ builder.rawQueryString = `$__escapeMulti(\\grafana-vm,\Network(eth0)\Total Bytes Received)`;
+ });
+
+ it('should not replace macro', () => {
+ const query = builder.generate().uriString;
+ expect(query).toContain(`%24__escapeMulti(%5Cgrafana-vm%2CNetwork(eth0)Total%20Bytes%20Received)`);
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/log_analytics/querystring_builder.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/log_analytics/querystring_builder.ts
new file mode 100644
index 00000000000..b206d48756a
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/log_analytics/querystring_builder.ts
@@ -0,0 +1,84 @@
+import moment from 'moment';
+
+export default class LogAnalyticsQuerystringBuilder {
+ constructor(public rawQueryString, public options, public defaultTimeField) {}
+
+ generate() {
+ let queryString = this.rawQueryString;
+ const macroRegexp = /\$__([_a-zA-Z0-9]+)\(([^\)]*)\)/gi;
+ queryString = queryString.replace(macroRegexp, (match, p1, p2) => {
+ if (p1 === 'contains') {
+ return this.getMultiContains(p2);
+ }
+
+ return match;
+ });
+
+ queryString = queryString.replace(/\$__escapeMulti\(('[^]*')\)/gi, (match, p1) => this.escape(p1));
+
+ if (this.options) {
+ queryString = queryString.replace(macroRegexp, (match, p1, p2) => {
+ if (p1 === 'timeFilter') {
+ return this.getTimeFilter(p2, this.options);
+ }
+
+ return match;
+ });
+ queryString = queryString.replace(/\$__interval/gi, this.options.interval);
+ queryString = queryString.replace(/\$__from/gi, this.getFrom(this.options));
+ queryString = queryString.replace(/\$__to/gi, this.getUntil(this.options));
+ }
+ const rawQuery = queryString;
+ queryString = encodeURIComponent(queryString);
+ const uriString = `query=${queryString}`;
+
+ return { uriString, rawQuery };
+ }
+
+ getFrom(options) {
+ const from = options.range.from;
+ return `datetime(${moment(from)
+ .startOf('minute')
+ .toISOString()})`;
+ }
+
+ getUntil(options) {
+ if (options.rangeRaw.to === 'now') {
+ return 'now()';
+ } else {
+ const until = options.range.to;
+ return `datetime(${moment(until)
+ .startOf('minute')
+ .toISOString()})`;
+ }
+ }
+
+ getTimeFilter(timeFieldArg, options) {
+ const timeField = timeFieldArg || this.defaultTimeField;
+ if (options.rangeRaw.to === 'now') {
+ return `${timeField} >= ${this.getFrom(options)}`;
+ } else {
+ return `${timeField} >= ${this.getFrom(options)} and ${timeField} <= ${this.getUntil(options)}`;
+ }
+ }
+
+ getMultiContains(inputs: string) {
+ const firstCommaIndex = inputs.indexOf(',');
+ const field = inputs.substring(0, firstCommaIndex);
+ const templateVar = inputs.substring(inputs.indexOf(',') + 1);
+
+ if (templateVar && templateVar.toLowerCase().trim() === 'all') {
+ return '1 == 1';
+ }
+
+ return `${field.trim()} in (${templateVar.trim()})`;
+ }
+
+ escape(inputs: string) {
+ return inputs
+ .substring(1, inputs.length - 1)
+ .split(`','`)
+ .map(v => `@'${v}'`)
+ .join(', ');
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.ts
new file mode 100644
index 00000000000..88af3e79851
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/module.ts
@@ -0,0 +1,11 @@
+import Datasource from './datasource';
+import { AzureMonitorQueryCtrl } from './query_ctrl';
+import { AzureMonitorAnnotationsQueryCtrl } from './annotations_query_ctrl';
+import { AzureMonitorConfigCtrl } from './config_ctrl';
+
+export {
+ Datasource,
+ AzureMonitorQueryCtrl as QueryCtrl,
+ AzureMonitorConfigCtrl as ConfigCtrl,
+ AzureMonitorAnnotationsQueryCtrl as AnnotationsQueryCtrl,
+};
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/annotations.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/annotations.editor.html
new file mode 100644
index 00000000000..e4a8c863984
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/annotations.editor.html
@@ -0,0 +1,64 @@
+
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html
new file mode 100644
index 00000000000..46ac8798680
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html
@@ -0,0 +1,196 @@
+Azure Monitor API Details
+
+
+
+Azure Log Analytics API Details
+
+
+ The Azure Log Analytics support is marked as being in a preview development state. This means it is in currently in active development and major changes might be made - depending on feedback from users.
+
+
+
+
+
+
+
+
+
+
+
+Application Insights Details
+
+
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html
new file mode 100644
index 00000000000..49f02ec8355
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html
@@ -0,0 +1,325 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/plugin.json b/public/app/plugins/datasource/grafana-azure-monitor-datasource/plugin.json
new file mode 100644
index 00000000000..76a56f2baaa
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/plugin.json
@@ -0,0 +1,162 @@
+{
+ "type": "datasource",
+ "name": "Azure Monitor",
+ "id": "grafana-azure-monitor-datasource",
+
+ "info": {
+ "description": "Grafana data source for Azure Monitor/Application Insights",
+ "author": {
+ "name": "Grafana Labs",
+ "url": "https://grafana.com"
+ },
+ "keywords": ["azure", "monitor", "Application Insights", "Log Analytics", "App Insights"],
+ "logos": {
+ "small": "img/logo.jpg",
+ "large": "img/logo.jpg"
+ },
+ "links": [
+ { "name": "Project site", "url": "https://github.com/grafana/azure-monitor-datasource" },
+ { "name": "Apache License", "url": "https://github.com/grafana/azure-monitor-datasource/blob/master/LICENSE" }
+ ],
+ "screenshots": [
+ { "name": "Azure Contoso Loans", "path": "img/contoso_loans_grafana_dashboard.png" },
+ { "name": "Azure Monitor Network", "path": "img/azure_monitor_network.png" },
+ { "name": "Azure Monitor CPU", "path": "img/azure_monitor_cpu.png" }
+ ],
+ "version": "0.3.0",
+ "updated": "2018-12-06"
+ },
+
+ "routes": [
+ {
+ "path": "azuremonitor",
+ "method": "GET",
+ "url": "https://management.azure.com",
+ "tokenAuth": {
+ "url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
+ "params": {
+ "grant_type": "client_credentials",
+ "client_id": "{{.JsonData.clientId}}",
+ "client_secret": "{{.SecureJsonData.clientSecret}}",
+ "resource": "https://management.azure.com/"
+ }
+ },
+ "headers": [{ "name": "x-ms-app", "content": "Grafana" }]
+ },
+ {
+ "path": "govazuremonitor",
+ "method": "GET",
+ "url": "https://management.usgovcloudapi.net",
+ "tokenAuth": {
+ "url": "https://login.microsoftonline.us/{{.JsonData.tenantId}}/oauth2/token",
+ "params": {
+ "grant_type": "client_credentials",
+ "client_id": "{{.JsonData.clientId}}",
+ "client_secret": "{{.SecureJsonData.clientSecret}}",
+ "resource": "https://management.usgovcloudapi.net/"
+ }
+ },
+ "headers": [{ "name": "x-ms-app", "content": "Grafana" }]
+ },
+ {
+ "path": "germanyazuremonitor",
+ "method": "GET",
+ "url": "https://management.microsoftazure.de",
+ "tokenAuth": {
+ "url": "https://login.microsoftonline.de/{{.JsonData.tenantId}}/oauth2/token",
+ "params": {
+ "grant_type": "client_credentials",
+ "client_id": "{{.JsonData.clientId}}",
+ "client_secret": "{{.SecureJsonData.clientSecret}}",
+ "resource": "https://management.microsoftazure.de/"
+ }
+ },
+ "headers": [{ "name": "x-ms-app", "content": "Grafana" }]
+ },
+ {
+ "path": "chinaazuremonitor",
+ "method": "GET",
+ "url": "https://management.chinacloudapi.cn",
+ "tokenAuth": {
+ "url": "https://login.chinacloudapi.cn/{{.JsonData.tenantId}}/oauth2/token",
+ "params": {
+ "grant_type": "client_credentials",
+ "client_id": "{{.JsonData.clientId}}",
+ "client_secret": "{{.SecureJsonData.clientSecret}}",
+ "resource": "https://management.chinacloudapi.cn/"
+ }
+ },
+ "headers": [{ "name": "x-ms-app", "content": "Grafana" }]
+ },
+ {
+ "path": "appinsights",
+ "method": "GET",
+ "url": "https://api.applicationinsights.io",
+ "headers": [
+ { "name": "X-API-Key", "content": "{{.SecureJsonData.appInsightsApiKey}}" },
+ { "name": "x-ms-app", "content": "Grafana" }
+ ]
+ },
+ {
+ "path": "workspacesloganalytics",
+ "method": "GET",
+ "url": "https://management.azure.com",
+ "tokenAuth": {
+ "url": "https://login.microsoftonline.com/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
+ "params": {
+ "grant_type": "client_credentials",
+ "client_id": "{{.JsonData.logAnalyticsClientId}}",
+ "client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
+ "resource": "https://management.azure.com/"
+ }
+ },
+ "headers": [{ "name": "x-ms-app", "content": "Grafana" }]
+ },
+ {
+ "path": "loganalyticsazure",
+ "method": "GET",
+ "url": "https://api.loganalytics.io/v1/workspaces",
+ "tokenAuth": {
+ "url": "https://login.microsoftonline.com/{{.JsonData.logAnalyticsTenantId}}/oauth2/token",
+ "params": {
+ "grant_type": "client_credentials",
+ "client_id": "{{.JsonData.logAnalyticsClientId}}",
+ "client_secret": "{{.SecureJsonData.logAnalyticsClientSecret}}",
+ "resource": "https://api.loganalytics.io"
+ }
+ },
+ "headers": [
+ { "name": "x-ms-app", "content": "Grafana" },
+ { "name": "Cache-Control", "content": "public, max-age=60" },
+ { "name": "Accept-Encoding", "content": "gzip" }
+ ]
+ },
+ {
+ "path": "sameasloganalyticsazure",
+ "method": "GET",
+ "url": "https://api.loganalytics.io/v1/workspaces",
+ "tokenAuth": {
+ "url": "https://login.microsoftonline.com/{{.JsonData.tenantId}}/oauth2/token",
+ "params": {
+ "grant_type": "client_credentials",
+ "client_id": "{{.JsonData.clientId}}",
+ "client_secret": "{{.SecureJsonData.clientSecret}}",
+ "resource": "https://api.loganalytics.io"
+ }
+ },
+ "headers": [
+ { "name": "x-ms-app", "content": "Grafana" },
+ { "name": "Cache-Control", "content": "public, max-age=60" },
+ { "name": "Accept-Encoding", "content": "gzip" }
+ ]
+ }
+ ],
+
+ "dependencies": {
+ "grafanaVersion": "5.2.x",
+ "plugins": []
+ },
+
+ "metrics": true,
+ "annotations": true
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.test.ts
new file mode 100644
index 00000000000..8d75ec87f3d
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.test.ts
@@ -0,0 +1,259 @@
+jest.mock('./css/query_editor.css', () => {
+ return {};
+});
+
+import { AzureMonitorQueryCtrl } from './query_ctrl';
+import Q from 'q';
+import { TemplateSrv } from 'app/features/templating/template_srv';
+
+describe('AzureMonitorQueryCtrl', () => {
+ let queryCtrl: any;
+
+ beforeEach(() => {
+ AzureMonitorQueryCtrl.prototype.panelCtrl = {
+ events: { on: () => {} },
+ panel: { scopedVars: [], targets: [] },
+ };
+ AzureMonitorQueryCtrl.prototype.target = {} as any;
+
+ queryCtrl = new AzureMonitorQueryCtrl({}, {}, new TemplateSrv());
+ queryCtrl.datasource = { $q: Q, appInsightsDatasource: { isConfigured: () => false } };
+ });
+
+ 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.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.metricName).toBe('select');
+ expect(queryCtrl.target.appInsights.groupBy).toBe('none');
+ });
+ });
+
+ describe('when the query type is Azure Monitor', () => {
+ describe('and getOptions for the Resource Group dropdown is called', () => {
+ const response = [{ text: 'nodeapp', value: 'nodeapp' }, { text: 'otherapp', value: 'otherapp' }];
+
+ beforeEach(() => {
+ queryCtrl.datasource.getResourceGroups = () => {
+ return queryCtrl.datasource.$q.when(response);
+ };
+ queryCtrl.datasource.azureMonitorDatasource = {
+ isConfigured: () => {
+ return true;
+ },
+ };
+ });
+
+ it('should return a list of Resource Groups', () => {
+ return queryCtrl.getResourceGroups('').then(result => {
+ expect(result[0].text).toBe('nodeapp');
+ });
+ });
+ });
+
+ describe('when getOptions for the Metric Definition dropdown is called', () => {
+ describe('and resource group has a value', () => {
+ const response = [
+ { text: 'Microsoft.Compute/virtualMachines', value: 'Microsoft.Compute/virtualMachines' },
+ { text: 'Microsoft.Network/publicIPAddresses', value: 'Microsoft.Network/publicIPAddresses' },
+ ];
+
+ beforeEach(() => {
+ queryCtrl.target.azureMonitor.resourceGroup = 'test';
+ queryCtrl.datasource.getMetricDefinitions = function(query) {
+ expect(query).toBe('test');
+ return this.$q.when(response);
+ };
+ });
+
+ it('should return a list of Metric Definitions', () => {
+ return queryCtrl.getMetricDefinitions('').then(result => {
+ expect(result[0].text).toBe('Microsoft.Compute/virtualMachines');
+ expect(result[1].text).toBe('Microsoft.Network/publicIPAddresses');
+ });
+ });
+ });
+
+ describe('and resource group has no value', () => {
+ beforeEach(() => {
+ queryCtrl.target.azureMonitor.resourceGroup = 'select';
+ });
+
+ it('should return without making a call to datasource', () => {
+ expect(queryCtrl.getMetricDefinitions('')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('when getOptions for the ResourceNames dropdown is called', () => {
+ describe('and resourceGroup and metricDefinition have values', () => {
+ const response = [{ text: 'test1', value: 'test1' }, { text: 'test2', value: 'test2' }];
+
+ beforeEach(() => {
+ queryCtrl.target.azureMonitor.resourceGroup = 'test';
+ queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
+ queryCtrl.datasource.getResourceNames = function(resourceGroup, metricDefinition) {
+ expect(resourceGroup).toBe('test');
+ expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
+ return this.$q.when(response);
+ };
+ });
+
+ it('should return a list of Resource Names', () => {
+ return queryCtrl.getResourceNames('').then(result => {
+ expect(result[0].text).toBe('test1');
+ expect(result[1].text).toBe('test2');
+ });
+ });
+ });
+
+ describe('and resourceGroup and metricDefinition do not have values', () => {
+ beforeEach(() => {
+ queryCtrl.target.azureMonitor.resourceGroup = 'select';
+ queryCtrl.target.azureMonitor.metricDefinition = 'select';
+ });
+
+ it('should return without making a call to datasource', () => {
+ expect(queryCtrl.getResourceNames('')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('when getOptions for the Metric Names dropdown is called', () => {
+ describe('and resourceGroup, metricDefinition and resourceName have values', () => {
+ const response = [{ text: 'metric1', value: 'metric1' }, { text: 'metric2', value: 'metric2' }];
+
+ beforeEach(() => {
+ queryCtrl.target.azureMonitor.resourceGroup = 'test';
+ queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
+ queryCtrl.target.azureMonitor.resourceName = 'test';
+ queryCtrl.datasource.getMetricNames = function(resourceGroup, metricDefinition, resourceName) {
+ expect(resourceGroup).toBe('test');
+ expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
+ expect(resourceName).toBe('test');
+ return this.$q.when(response);
+ };
+ });
+
+ it('should return a list of Metric Names', () => {
+ return queryCtrl.getMetricNames('').then(result => {
+ expect(result[0].text).toBe('metric1');
+ expect(result[1].text).toBe('metric2');
+ });
+ });
+ });
+
+ describe('and resourceGroup, metricDefinition and resourceName do not have values', () => {
+ beforeEach(() => {
+ queryCtrl.target.azureMonitor.resourceGroup = 'select';
+ queryCtrl.target.azureMonitor.metricDefinition = 'select';
+ queryCtrl.target.azureMonitor.resourceName = 'select';
+ });
+
+ it('should return without making a call to datasource', () => {
+ expect(queryCtrl.getMetricNames('')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('when onMetricNameChange is triggered for the Metric Names dropdown', () => {
+ const response = {
+ primaryAggType: 'Average',
+ supportAggOptions: ['Average', 'Total'],
+ supportedTimeGrains: ['PT1M', 'P1D'],
+ dimensions: [],
+ };
+
+ beforeEach(() => {
+ queryCtrl.target.azureMonitor.resourceGroup = 'test';
+ queryCtrl.target.azureMonitor.metricDefinition = 'Microsoft.Compute/virtualMachines';
+ queryCtrl.target.azureMonitor.resourceName = 'test';
+ queryCtrl.target.azureMonitor.metricName = 'Percentage CPU';
+ queryCtrl.datasource.getMetricMetadata = function(resourceGroup, metricDefinition, resourceName, metricName) {
+ expect(resourceGroup).toBe('test');
+ expect(metricDefinition).toBe('Microsoft.Compute/virtualMachines');
+ expect(resourceName).toBe('test');
+ expect(metricName).toBe('Percentage CPU');
+ return this.$q.when(response);
+ };
+ });
+
+ it('should set the options and default selected value for the Aggregations dropdown', () => {
+ queryCtrl.onMetricNameChange().then(() => {
+ expect(queryCtrl.target.azureMonitor.aggregation).toBe('Average');
+ expect(queryCtrl.target.azureMonitor.aggOptions).toBe(['Average', 'Total']);
+ expect(queryCtrl.target.azureMonitor.timeGrains).toBe(['PT1M', 'P1D']);
+ });
+ });
+ });
+ });
+
+ describe('and query type is Application Insights', () => {
+ 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 queryCtrl.datasource.$q.when(response);
+ };
+ });
+
+ it('should return a list of Metric Names', () => {
+ return queryCtrl.getAppInsightsMetricNames().then(result => {
+ 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.groupByOptions = ['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 = function(metricName) {
+ expect(metricName).toBe('requests/failed');
+ return this.$q.when(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.groupByOptions).toContain('client/os');
+ expect(queryCtrl.target.appInsights.groupByOptions).toContain('client/city');
+ });
+ });
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts
new file mode 100644
index 00000000000..fd42c172f11
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts
@@ -0,0 +1,404 @@
+import _ from 'lodash';
+import { QueryCtrl } from 'app/plugins/sdk';
+// import './css/query_editor.css';
+import TimegrainConverter from './time_grain_converter';
+// import './monaco/kusto_monaco_editor';
+import './editor/editor_component';
+
+export interface ResultFormat {
+ text: string;
+ value: string;
+}
+
+export class AzureMonitorQueryCtrl extends QueryCtrl {
+ static templateUrl = 'partials/query.editor.html';
+
+ defaultDropdownValue = 'select';
+
+ target: {
+ refId: string;
+ queryType: string;
+ azureMonitor: {
+ resourceGroup: string;
+ resourceName: string;
+ metricDefinition: string;
+ metricName: string;
+ dimensionFilter: string;
+ timeGrain: string;
+ timeGrainUnit: string;
+ timeGrains: any[];
+ dimensions: any[];
+ dimension: any;
+ aggregation: string;
+ aggOptions: string[];
+ };
+ azureLogAnalytics: {
+ query: string;
+ resultFormat: string;
+ workspace: string;
+ };
+ appInsights: {
+ metricName: string;
+ rawQuery: boolean;
+ rawQueryString: string;
+ groupBy: string;
+ timeGrainType: string;
+ xaxis: string;
+ yaxis: string;
+ spliton: string;
+ aggOptions: string[];
+ aggregation: string;
+ groupByOptions: string[];
+ timeGrainUnit: string;
+ timeGrain: string;
+ };
+ };
+
+ defaults = {
+ queryType: 'Azure Monitor',
+ azureMonitor: {
+ resourceGroup: this.defaultDropdownValue,
+ metricDefinition: this.defaultDropdownValue,
+ resourceName: this.defaultDropdownValue,
+ metricName: this.defaultDropdownValue,
+ dimensionFilter: '*',
+ timeGrain: 'auto',
+ },
+ azureLogAnalytics: {
+ query: [
+ '//change this example to create your own time series query',
+ ' ' +
+ '//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 , 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
+ : '',
+ },
+ appInsights: {
+ metricName: this.defaultDropdownValue,
+ rawQuery: false,
+ rawQueryString: '',
+ groupBy: 'none',
+ timeGrainType: 'auto',
+ xaxis: 'timestamp',
+ yaxis: '',
+ spliton: '',
+ },
+ };
+
+ resultFormats: ResultFormat[];
+ workspaces: any[];
+ showHelp: boolean;
+ showLastQuery: boolean;
+ lastQuery: string;
+ lastQueryError?: string;
+
+ /** @ngInject */
+ constructor($scope, $injector, private templateSrv) {
+ super($scope, $injector);
+
+ _.defaultsDeep(this.target, this.defaults);
+
+ this.migrateTimeGrains();
+
+ this.panelCtrl.events.on('data-received', this.onDataReceived.bind(this), $scope);
+ this.panelCtrl.events.on('data-error', this.onDataError.bind(this), $scope);
+ this.resultFormats = [{ text: 'Time series', value: 'time_series' }, { text: 'Table', value: 'table' }];
+ if (this.target.queryType === 'Azure Log Analytics') {
+ this.getWorkspaces();
+ }
+ }
+
+ onDataReceived(dataList) {
+ this.lastQueryError = undefined;
+ this.lastQuery = '';
+
+ const anySeriesFromQuery: any = _.find(dataList, { refId: this.target.refId });
+ if (anySeriesFromQuery) {
+ this.lastQuery = anySeriesFromQuery.query;
+ }
+ }
+
+ onDataError(err) {
+ this.handleQueryCtrlError(err);
+ }
+
+ handleQueryCtrlError(err) {
+ 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,
+ this.target.azureMonitor.timeGrainUnit
+ );
+ }
+
+ delete this.target.azureMonitor.timeGrainUnit;
+ this.onMetricNameChange();
+ }
+ }
+
+ replace(variable: string) {
+ return this.templateSrv.replace(variable, this.panelCtrl.panel.scopedVars);
+ }
+
+ onQueryTypeChange() {
+ if (this.target.queryType === 'Azure Log Analytics') {
+ return this.getWorkspaces();
+ }
+ }
+
+ /* Azure Monitor Section */
+ getResourceGroups(query) {
+ if (this.target.queryType !== 'Azure Monitor' || !this.datasource.azureMonitorDatasource.isConfigured()) {
+ return;
+ }
+
+ return this.datasource.getResourceGroups().catch(this.handleQueryCtrlError.bind(this));
+ }
+
+ getMetricDefinitions(query) {
+ if (
+ this.target.queryType !== 'Azure Monitor' ||
+ !this.target.azureMonitor.resourceGroup ||
+ this.target.azureMonitor.resourceGroup === this.defaultDropdownValue
+ ) {
+ return;
+ }
+ return this.datasource
+ .getMetricDefinitions(this.replace(this.target.azureMonitor.resourceGroup))
+ .catch(this.handleQueryCtrlError.bind(this));
+ }
+
+ getResourceNames(query) {
+ if (
+ this.target.queryType !== 'Azure Monitor' ||
+ !this.target.azureMonitor.resourceGroup ||
+ this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
+ !this.target.azureMonitor.metricDefinition ||
+ this.target.azureMonitor.metricDefinition === this.defaultDropdownValue
+ ) {
+ return;
+ }
+
+ return this.datasource
+ .getResourceNames(
+ this.replace(this.target.azureMonitor.resourceGroup),
+ this.replace(this.target.azureMonitor.metricDefinition)
+ )
+ .catch(this.handleQueryCtrlError.bind(this));
+ }
+
+ getMetricNames(query) {
+ if (
+ this.target.queryType !== 'Azure Monitor' ||
+ !this.target.azureMonitor.resourceGroup ||
+ this.target.azureMonitor.resourceGroup === this.defaultDropdownValue ||
+ !this.target.azureMonitor.metricDefinition ||
+ this.target.azureMonitor.metricDefinition === this.defaultDropdownValue ||
+ !this.target.azureMonitor.resourceName ||
+ this.target.azureMonitor.resourceName === this.defaultDropdownValue
+ ) {
+ return;
+ }
+
+ return this.datasource
+ .getMetricNames(
+ this.replace(this.target.azureMonitor.resourceGroup),
+ this.replace(this.target.azureMonitor.metricDefinition),
+ this.replace(this.target.azureMonitor.resourceName)
+ )
+ .catch(this.handleQueryCtrlError.bind(this));
+ }
+
+ onResourceGroupChange() {
+ this.target.azureMonitor.metricDefinition = this.defaultDropdownValue;
+ this.target.azureMonitor.resourceName = this.defaultDropdownValue;
+ this.target.azureMonitor.metricName = this.defaultDropdownValue;
+ this.target.azureMonitor.dimensions = [];
+ this.target.azureMonitor.dimension = '';
+ }
+
+ onMetricDefinitionChange() {
+ this.target.azureMonitor.resourceName = this.defaultDropdownValue;
+ this.target.azureMonitor.metricName = this.defaultDropdownValue;
+ this.target.azureMonitor.dimensions = [];
+ this.target.azureMonitor.dimension = '';
+ }
+
+ onResourceNameChange() {
+ this.target.azureMonitor.metricName = this.defaultDropdownValue;
+ this.target.azureMonitor.dimensions = [];
+ this.target.azureMonitor.dimension = '';
+ }
+
+ onMetricNameChange() {
+ if (!this.target.azureMonitor.metricName || this.target.azureMonitor.metricName === this.defaultDropdownValue) {
+ return;
+ }
+
+ return this.datasource
+ .getMetricMetadata(
+ this.replace(this.target.azureMonitor.resourceGroup),
+ this.replace(this.target.azureMonitor.metricDefinition),
+ this.replace(this.target.azureMonitor.resourceName),
+ this.replace(this.target.azureMonitor.metricName)
+ )
+ .then(metadata => {
+ this.target.azureMonitor.aggOptions = metadata.supportedAggTypes || [metadata.primaryAggType];
+ this.target.azureMonitor.aggregation = metadata.primaryAggType;
+ this.target.azureMonitor.timeGrains = [{ text: 'auto', value: 'auto' }].concat(metadata.supportedTimeGrains);
+
+ this.target.azureMonitor.dimensions = metadata.dimensions;
+ if (metadata.dimensions.length > 0) {
+ this.target.azureMonitor.dimension = metadata.dimensions[0].value;
+ }
+ return this.refresh();
+ })
+ .catch(this.handleQueryCtrlError.bind(this));
+ }
+
+ getAutoInterval() {
+ if (this.target.azureMonitor.timeGrain === 'auto') {
+ return TimegrainConverter.findClosestTimeGrain(
+ this.templateSrv.builtIns.__interval.value,
+ _.map(this.target.azureMonitor.timeGrains, o =>
+ TimegrainConverter.createKbnUnitFromISO8601Duration(o.value)
+ ) || ['1m', '5m', '15m', '30m', '1h', '6h', '12h', '1d']
+ );
+ }
+
+ return '';
+ }
+
+ /* Azure Log Analytics */
+
+ getWorkspaces() {
+ return this.datasource.azureLogAnalyticsDatasource
+ .getWorkspaces()
+ .then(list => {
+ this.workspaces = list;
+ if (list.length > 0 && !this.target.azureLogAnalytics.workspace) {
+ this.target.azureLogAnalytics.workspace = list[0].value;
+ }
+ })
+ .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.variables.map(t => '$' + t.name);
+ }
+
+ /* Application Insights Section */
+
+ getAppInsightsAutoInterval() {
+ const interval = this.templateSrv.builtIns.__interval.value;
+ if (interval[interval.length - 1] === 's') {
+ return '1m';
+ }
+ return interval;
+ }
+ 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 => {
+ this.target.appInsights.aggOptions = aggData.supportedAggTypes;
+ this.target.appInsights.groupByOptions = aggData.supportedGroupBy;
+ this.target.appInsights.aggregation = aggData.primaryAggType;
+ return this.refresh();
+ })
+ .catch(this.handleQueryCtrlError.bind(this));
+ }
+
+ getAppInsightsGroupBySegments(query) {
+ return _.map(this.target.appInsights.groupByOptions, option => {
+ return { text: option, value: option };
+ });
+ }
+
+ resetAppInsightsGroupBy() {
+ this.target.appInsights.groupBy = 'none';
+ this.refresh();
+ }
+
+ updateTimeGrainType() {
+ if (this.target.appInsights.timeGrainType === 'specific') {
+ this.target.appInsights.timeGrain = '1';
+ this.target.appInsights.timeGrainUnit = 'minute';
+ } else {
+ this.target.appInsights.timeGrain = '';
+ }
+ this.refresh();
+ }
+
+ toggleEditorMode() {
+ this.target.appInsights.rawQuery = !this.target.appInsights.rawQuery;
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_help.md b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_help.md
new file mode 100644
index 00000000000..428430fbdac
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_help.md
@@ -0,0 +1,34 @@
+Format the legend keys any way you want by using alias patterns.
+
+- Example for Azure Monitor: `dimension: {{dimensionvalue}}`
+- Example for Application Insights: `server: {{groupbyvalue}}`
+
+#### Alias Patterns for Application Insights
+
+- {{groupbyvalue}} = replaced with the value of the group by
+- {{groupbyname}} = replaced with the name/label of the group by
+- {{metric}} = replaced with metric name (e.g. requests/count)
+
+#### Alias Patterns for Azure Monitor
+
+- {{resourcegroup}} = replaced with the value of the Resource Group
+- {{namespace}} = replaced with the value of the Namespace (e.g. Microsoft.Compute/virtualMachines)
+- {{resourcename}} = replaced with the value of the Resource Name
+- {{metric}} = replaced with metric name (e.g. Percentage CPU)
+- {{dimensionname}} = replaced with dimension key/label (e.g. blobtype)
+- {{dimensionvalue}} = replaced with dimension value (e.g. BlockBlob)
+
+#### Filter Expressions for Application Insights
+
+The filter field takes an OData filter expression.
+
+Examples:
+
+- `client/city eq 'Boydton'`
+- `client/city ne 'Boydton'`
+- `client/city ne 'Boydton' and client/city ne 'Dublin'`
+- `client/city eq 'Boydton' or client/city eq 'Dublin'`
+
+#### Writing Queries for Template Variables
+
+See the [docs](https://github.com/grafana/azure-monitor-datasource#templating-with-variables) for details and examples.
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/time_grain_converter.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/time_grain_converter.test.ts
new file mode 100644
index 00000000000..f5e1eefb1cd
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/time_grain_converter.test.ts
@@ -0,0 +1,23 @@
+import TimeGrainConverter from './time_grain_converter';
+
+describe('TimeGrainConverter', () => {
+ describe('with duration of PT1H', () => {
+ it('should convert it to text', () => {
+ expect(TimeGrainConverter.createTimeGrainFromISO8601Duration('PT1H')).toEqual('1 hour');
+ });
+
+ it('should convert it to kbn', () => {
+ expect(TimeGrainConverter.createKbnUnitFromISO8601Duration('PT1H')).toEqual('1h');
+ });
+ });
+
+ describe('with duration of P1D', () => {
+ it('should convert it to text', () => {
+ expect(TimeGrainConverter.createTimeGrainFromISO8601Duration('P1D')).toEqual('1 day');
+ });
+
+ it('should convert it to kbn', () => {
+ expect(TimeGrainConverter.createKbnUnitFromISO8601Duration('P1D')).toEqual('1d');
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/time_grain_converter.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/time_grain_converter.ts
new file mode 100644
index 00000000000..9604fe33e57
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/time_grain_converter.ts
@@ -0,0 +1,122 @@
+import _ from 'lodash';
+import kbn from 'app/core/utils/kbn';
+
+export default class TimeGrainConverter {
+ static createISO8601Duration(timeGrain, timeGrainUnit) {
+ const timeIntervals = ['hour', 'minute', 'h', 'm'];
+ if (_.includes(timeIntervals, timeGrainUnit)) {
+ return `PT${timeGrain}${timeGrainUnit[0].toUpperCase()}`;
+ }
+
+ return `P${timeGrain}${timeGrainUnit[0].toUpperCase()}`;
+ }
+
+ static createISO8601DurationFromInterval(interval: string) {
+ const timeGrain = +interval.slice(0, interval.length - 1);
+ const unit = interval[interval.length - 1];
+
+ if (interval.indexOf('ms') > -1) {
+ return TimeGrainConverter.createISO8601Duration(1, 'm');
+ }
+
+ if (interval[interval.length - 1] === 's') {
+ let toMinutes = (timeGrain * 60) % 60;
+
+ if (toMinutes < 1) {
+ toMinutes = 1;
+ }
+
+ return TimeGrainConverter.createISO8601Duration(toMinutes, 'm');
+ }
+
+ return TimeGrainConverter.createISO8601Duration(timeGrain, unit);
+ }
+
+ static findClosestTimeGrain(interval, allowedTimeGrains) {
+ const timeGrains = _.filter(allowedTimeGrains, o => o !== 'auto');
+
+ let closest = timeGrains[0];
+ const intervalMs = kbn.interval_to_ms(interval);
+
+ for (let i = 0; i < timeGrains.length; i++) {
+ // abs (num - val) < abs (num - curr):
+ if (intervalMs > kbn.interval_to_ms(timeGrains[i])) {
+ if (i + 1 < timeGrains.length) {
+ closest = timeGrains[i + 1];
+ } else {
+ closest = timeGrains[i];
+ }
+ }
+ }
+
+ return closest;
+ }
+
+ static createTimeGrainFromISO8601Duration(duration: string) {
+ let offset = 1;
+ if (duration.substring(0, 2) === 'PT') {
+ offset = 2;
+ }
+
+ const value = duration.substring(offset, duration.length - 1);
+ const unit = duration.substring(duration.length - 1);
+
+ return value + ' ' + TimeGrainConverter.timeUnitToText(+value, unit);
+ }
+
+ static timeUnitToText(value: number, unit: string) {
+ let text = '';
+
+ if (unit === 'S') {
+ text = 'second';
+ }
+ if (unit === 'M') {
+ text = 'minute';
+ }
+ if (unit === 'H') {
+ text = 'hour';
+ }
+ if (unit === 'D') {
+ text = 'day';
+ }
+
+ if (value > 1) {
+ return text + 's';
+ }
+
+ return text;
+ }
+
+ static createKbnUnitFromISO8601Duration(duration: string) {
+ if (duration === 'auto') {
+ return 'auto';
+ }
+
+ let offset = 1;
+ if (duration.substring(0, 2) === 'PT') {
+ offset = 2;
+ }
+
+ const value = duration.substring(offset, duration.length - 1);
+ const unit = duration.substring(duration.length - 1);
+
+ return value + TimeGrainConverter.timeUnitToKbn(+value, unit);
+ }
+
+ static timeUnitToKbn(value: number, unit: string) {
+ if (unit === 'S') {
+ return 's';
+ }
+ if (unit === 'M') {
+ return 'm';
+ }
+ if (unit === 'H') {
+ return 'h';
+ }
+ if (unit === 'D') {
+ return 'd';
+ }
+
+ return '';
+ }
+}
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts
new file mode 100644
index 00000000000..17a6ce9bb0b
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts
@@ -0,0 +1,53 @@
+import { SemVersion, isVersionGtOrEq } from './version';
+
+describe('SemVersion', () => {
+ let version = '1.0.0-alpha.1';
+
+ describe('parsing', () => {
+ it('should parse version properly', () => {
+ const semver = new SemVersion(version);
+ expect(semver.major).toBe(1);
+ expect(semver.minor).toBe(0);
+ expect(semver.patch).toBe(0);
+ expect(semver.meta).toBe('alpha.1');
+ });
+ });
+
+ describe('comparing', () => {
+ beforeEach(() => {
+ version = '3.4.5';
+ });
+
+ it('should detect greater version properly', () => {
+ const semver = new SemVersion(version);
+ const cases = [
+ { value: '3.4.5', expected: true },
+ { value: '3.4.4', expected: true },
+ { value: '3.4.6', expected: false },
+ { value: '4', expected: false },
+ { value: '3.5', expected: false },
+ ];
+ cases.forEach(testCase => {
+ expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected);
+ });
+ });
+ });
+
+ describe('isVersionGtOrEq', () => {
+ it('should compare versions properly (a >= b)', () => {
+ const cases = [
+ { values: ['3.4.5', '3.4.5'], expected: true },
+ { values: ['3.4.5', '3.4.4'], expected: true },
+ { values: ['3.4.5', '3.4.6'], expected: false },
+ { values: ['3.4', '3.4.0'], expected: true },
+ { values: ['3', '3.0.0'], expected: true },
+ { values: ['3.1.1-beta1', '3.1'], expected: true },
+ { values: ['3.4.5', '4'], expected: false },
+ { values: ['3.4.5', '3.5'], expected: false },
+ ];
+ cases.forEach(testCase => {
+ expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected);
+ });
+ });
+ });
+});
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts
new file mode 100644
index 00000000000..1131e1d2ab8
--- /dev/null
+++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts
@@ -0,0 +1,34 @@
+import _ from 'lodash';
+
+const versionPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z\.]+))?/;
+
+export class SemVersion {
+ major: number;
+ minor: number;
+ patch: number;
+ meta: string;
+
+ constructor(version: string) {
+ const match = versionPattern.exec(version);
+ if (match) {
+ this.major = Number(match[1]);
+ this.minor = Number(match[2] || 0);
+ this.patch = Number(match[3] || 0);
+ this.meta = match[4];
+ }
+ }
+
+ isGtOrEq(version: string): boolean {
+ const compared = new SemVersion(version);
+ return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch);
+ }
+
+ isValid(): boolean {
+ return _.isNumber(this.major);
+ }
+}
+
+export function isVersionGtOrEq(a: string, b: string): boolean {
+ const aSemver = new SemVersion(a);
+ return aSemver.isGtOrEq(b);
+}
diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx
index c50f80fa435..ccaca86d880 100644
--- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx
+++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx
@@ -17,6 +17,7 @@ import LokiDatasource from '../datasource';
// Types
import { LokiQuery } from '../types';
import { TypeaheadOutput } from 'app/types/explore';
+import { makePromiseCancelable, CancelablePromise } from 'app/core/utils/CancelablePromise';
const PRISM_SYNTAX = 'promql';
@@ -86,6 +87,7 @@ export class LokiQueryField extends React.PureComponent;
constructor(props: LokiQueryFieldProps, context) {
super(props, context);
@@ -113,12 +115,24 @@ export class LokiQueryField extends React.PureComponent {
remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
})
- .then(() => this.onUpdateLanguage());
+ .then(() => this.onUpdateLanguage())
+ .catch(({ isCanceled }) => {
+ if (isCanceled) {
+ console.warn('LokiQueryField has unmounted, language provider intialization was canceled');
+ }
+ });
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.languageProviderInitializationPromise) {
+ this.languageProviderInitializationPromise.cancel();
}
}
@@ -213,26 +227,30 @@ export class LokiQueryField extends React.PureComponent
-
-
-
- {chooserText}
-
-
+ <>
+
+
+
+
+ {chooserText}
+
+
+
+
+
+
-
-
+
{error ?
{error}
: null}
{hint ? (
@@ -245,7 +263,7 @@ export class LokiQueryField extends React.PureComponent
) : null}
-
+ >
);
}
}
diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
index f5b5b311b2a..9d68a093cc3 100644
--- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
+++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
@@ -12,6 +12,7 @@ import BracesPlugin from 'app/features/explore/slate-plugins/braces';
import RunnerPlugin from 'app/features/explore/slate-plugins/runner';
import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField';
import { PromQuery } from '../types';
+import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise';
const HISTOGRAM_GROUP = '__histograms__';
const METRIC_MARK = 'metric';
@@ -104,6 +105,7 @@ interface PromQueryFieldState {
class PromQueryField extends React.PureComponent
{
plugins: any[];
languageProvider: any;
+ languageProviderInitializationPromise: CancelablePromise;
constructor(props: PromQueryFieldProps, context) {
super(props, context);
@@ -129,12 +131,23 @@ class PromQueryField extends React.PureComponent {
remaining.map(task => task.then(this.onUpdateLanguage).catch(() => {}));
})
- .then(() => this.onUpdateLanguage());
+ .then(() => this.onUpdateLanguage())
+ .catch(({ isCanceled }) => {
+ if (isCanceled) {
+ console.warn('PromQueryField has unmounted, language provider intialization was canceled');
+ }
+ });
+ }
+ }
+
+ componentWillUnmount() {
+ if (this.languageProviderInitializationPromise) {
+ this.languageProviderInitializationPromise.cancel();
}
}
@@ -235,39 +248,41 @@ class PromQueryField extends React.PureComponent
-
-
-
- {chooserText}
-
-
+ <>
+
+
+
+
+ {chooserText}
+
+
+
+
+
+
-
-
- {error ?
{error}
: null}
- {hint ? (
-
- ) : null}
-
-
+ {error ? {error}
: null}
+ {hint ? (
+
+ ) : null}
+ >
);
}
}
diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts
index 3e29747cc95..025955105a7 100644
--- a/public/app/plugins/datasource/stackdriver/datasource.ts
+++ b/public/app/plugins/datasource/stackdriver/datasource.ts
@@ -34,7 +34,7 @@ export default class StackdriverDatasource {
intervalMs: options.intervalMs,
datasourceId: this.id,
metricType: this.templateSrv.replace(t.metricType, options.scopedVars || {}),
- primaryAggregation: this.templateSrv.replace(t.crossSeriesReducer || 'REDUCE_MEAN', options.scopedVars || {}),
+ crossSeriesReducer: this.templateSrv.replace(t.crossSeriesReducer || 'REDUCE_MEAN', options.scopedVars || {}),
perSeriesAligner: this.templateSrv.replace(t.perSeriesAligner, options.scopedVars || {}),
alignmentPeriod: this.templateSrv.replace(t.alignmentPeriod, options.scopedVars || {}),
groupBys: this.interpolateGroupBys(t.groupBys, options.scopedVars),
@@ -138,7 +138,7 @@ export default class StackdriverDatasource {
refId: 'annotationQuery',
datasourceId: this.id,
metricType: this.templateSrv.replace(annotation.target.metricType, options.scopedVars || {}),
- primaryAggregation: 'REDUCE_NONE',
+ crossSeriesReducer: 'REDUCE_NONE',
perSeriesAligner: 'ALIGN_NONE',
title: this.templateSrv.replace(annotation.target.title, options.scopedVars || {}),
text: this.templateSrv.replace(annotation.target.text, options.scopedVars || {}),
diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json
index d92127502c7..e68100c0e59 100644
--- a/public/app/plugins/datasource/stackdriver/plugin.json
+++ b/public/app/plugins/datasource/stackdriver/plugin.json
@@ -6,7 +6,6 @@
"alerting": true,
"annotations": true,
"tables": false,
- "state": "beta",
"queryOptions": {
"maxDataPoints": true,
"cacheTimeout": true
diff --git a/public/app/plugins/panel/gauge/plugin.json b/public/app/plugins/panel/gauge/plugin.json
index c3fea3387d2..58437779d25 100644
--- a/public/app/plugins/panel/gauge/plugin.json
+++ b/public/app/plugins/panel/gauge/plugin.json
@@ -3,8 +3,6 @@
"name": "Gauge",
"id": "gauge",
- "state": "alpha",
-
"info": {
"author": {
"name": "Grafana Project",
diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts
index 78f9d09520b..aeb540551b8 100755
--- a/public/app/plugins/panel/graph/graph.ts
+++ b/public/app/plugins/panel/graph/graph.ts
@@ -11,7 +11,6 @@ import './jquery.flot.events';
import $ from 'jquery';
import _ from 'lodash';
import moment from 'moment';
-import kbn from 'app/core/utils/kbn';
import { tickStep } from 'app/core/utils/ticks';
import { appEvents, coreModule, updateLegendValues } from 'app/core/core';
import GraphTooltip from './graph_tooltip';
@@ -26,7 +25,7 @@ import ReactDOM from 'react-dom';
import { Legend, GraphLegendProps } from './Legend/Legend';
import { GraphCtrl } from './module';
-import { GrafanaTheme } from '@grafana/ui';
+import { GrafanaTheme, getValueFormat } from '@grafana/ui';
class GraphElement {
ctrl: GraphCtrl;
@@ -730,10 +729,12 @@ class GraphElement {
configureAxisMode(axis, format) {
axis.tickFormatter = (val, axis) => {
- if (!kbn.valueFormats[format]) {
+ const formatter = getValueFormat(format);
+
+ if (!formatter) {
throw new Error(`Unit '${format}' is not supported`);
}
- return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
+ return formatter(val, axis.tickDecimals, axis.scaledDecimals);
};
}
diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts
index fc0b86d1b68..691247a0f75 100644
--- a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts
+++ b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts
@@ -43,6 +43,25 @@ describe('TimeRegionManager', () => {
});
}
+ describe('When colors missing in config', () => {
+ plotOptionsScenario('should not throw an error when fillColor is undefined', ctx => {
+ const regions = [
+ { fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, lineColor: '#ffffff', colorMode: 'custom' },
+ ];
+ const from = moment('2018-01-01T00:00:00+01:00');
+ const to = moment('2018-01-01T23:59:00+01:00');
+ expect(() => ctx.setup(regions, from, to)).not.toThrow();
+ });
+ plotOptionsScenario('should not throw an error when lineColor is undefined', ctx => {
+ const regions = [
+ { fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, fillColor: '#ffffff', line: true, colorMode: 'custom' },
+ ];
+ const from = moment('2018-01-01T00:00:00+01:00');
+ const to = moment('2018-01-01T23:59:00+01:00');
+ expect(() => ctx.setup(regions, from, to)).not.toThrow();
+ });
+ });
+
describe('When creating plot markings using local time', () => {
plotOptionsScenario('for day of week region', ctx => {
const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }];
diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts
index be5de722fe2..2917583ff36 100644
--- a/public/app/plugins/panel/graph/time_region_manager.ts
+++ b/public/app/plugins/panel/graph/time_region_manager.ts
@@ -50,8 +50,8 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition {
if (timeRegion.colorMode === 'custom') {
return {
- fill: getColorFromHexRgbOrName(timeRegion.fillColor, theme),
- line: getColorFromHexRgbOrName(timeRegion.lineColor, theme),
+ fill: timeRegion.fill && timeRegion.fillColor ? getColorFromHexRgbOrName(timeRegion.fillColor, theme) : null,
+ line: timeRegion.line && timeRegion.lineColor ? getColorFromHexRgbOrName(timeRegion.lineColor, theme) : null,
};
}
@@ -62,8 +62,8 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition {
}
return {
- fill: getColorFromHexRgbOrName(colorMode.color.fill, theme),
- line: getColorFromHexRgbOrName(colorMode.color.line, theme),
+ fill: timeRegion.fill ? getColorFromHexRgbOrName(colorMode.color.fill, theme) : null,
+ line: timeRegion.fill ? getColorFromHexRgbOrName(colorMode.color.line, theme) : null,
};
}
diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
index 4ff0176d0bf..be9920b3edf 100644
--- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
+++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts
@@ -1,8 +1,8 @@
import * as d3 from 'd3';
import $ from 'jquery';
import _ from 'lodash';
-import kbn from 'app/core/utils/kbn';
import { getValueBucketBound } from './heatmap_data_converter';
+import { getValueFormat } from '@grafana/ui';
const TOOLTIP_PADDING_X = 30;
const TOOLTIP_PADDING_Y = 5;
@@ -268,7 +268,7 @@ export class HeatmapTooltip {
countValueFormatter(decimals, scaledDecimals = null) {
const format = 'short';
return value => {
- return kbn.valueFormats[format](value, decimals, scaledDecimals);
+ return getValueFormat(format)(value, decimals, scaledDecimals);
};
}
}
diff --git a/public/app/plugins/panel/heatmap/rendering.ts b/public/app/plugins/panel/heatmap/rendering.ts
index 6333a985819..6489c9e9895 100644
--- a/public/app/plugins/panel/heatmap/rendering.ts
+++ b/public/app/plugins/panel/heatmap/rendering.ts
@@ -2,13 +2,12 @@ import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment';
import * as d3 from 'd3';
-import kbn from 'app/core/utils/kbn';
import { appEvents, contextSrv } from 'app/core/core';
import * as ticksUtils from 'app/core/utils/ticks';
import { HeatmapTooltip } from './heatmap_tooltip';
import { mergeZeroBuckets } from './heatmap_data_converter';
import { getColorScale, getOpacityScale } from './color_scale';
-import { GrafanaTheme, getColorFromHexRgbOrName } from '@grafana/ui';
+import { GrafanaTheme, getColorFromHexRgbOrName, getValueFormat } from '@grafana/ui';
const MIN_CARD_SIZE = 1,
CARD_PADDING = 1,
@@ -436,7 +435,7 @@ export class HeatmapRenderer {
const format = this.panel.yAxis.format;
return value => {
try {
- return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value;
+ return format !== 'none' ? getValueFormat(format)(value, decimals, scaledDecimals) : value;
} catch (err) {
console.error(err.message || err);
return value;
diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts
index da9cf0de689..2768951d2ba 100644
--- a/public/app/plugins/panel/singlestat/module.ts
+++ b/public/app/plugins/panel/singlestat/module.ts
@@ -8,8 +8,7 @@ import kbn from 'app/core/utils/kbn';
import config from 'app/core/config';
import TimeSeries from 'app/core/time_series2';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
-import { getColorFromHexRgbOrName } from '@grafana/ui';
-import { GrafanaTheme } from '@grafana/ui';
+import { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
class SingleStatCtrl extends MetricsPanelCtrl {
static templateUrl = 'module.html';
@@ -192,7 +191,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.valueRounded = 0;
} else {
const decimalInfo = this.getDecimalsForValue(data.value);
- const formatFunc = kbn.valueFormats[this.panel.format];
+ const formatFunc = getValueFormat(this.panel.format);
+
data.valueFormatted = formatFunc(
datapoint[this.panel.tableColumn],
decimalInfo.decimals,
@@ -301,6 +301,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
if (this.series && this.series.length > 0) {
const lastPoint = _.last(this.series[0].datapoints);
const lastValue = _.isArray(lastPoint) ? lastPoint[0] : null;
+ const formatFunc = getValueFormat(this.panel.format);
if (this.panel.valueName === 'name') {
data.value = 0;
@@ -311,7 +312,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.valueFormatted = _.escape(lastValue);
data.valueRounded = 0;
} else if (this.panel.valueName === 'last_time') {
- const formatFunc = kbn.valueFormats[this.panel.format];
data.value = lastPoint[1];
data.valueRounded = data.value;
data.valueFormatted = formatFunc(data.value, 0, 0, this.dashboard.isTimezoneUtc());
@@ -320,7 +320,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
data.flotpairs = this.series[0].flotpairs;
const decimalInfo = this.getDecimalsForValue(data.value);
- const formatFunc = kbn.valueFormats[this.panel.format];
data.valueFormatted = formatFunc(
data.value,
diff --git a/public/app/plugins/panel/table/module.ts b/public/app/plugins/panel/table/module.ts
index ebc654f8be9..82763e1839a 100644
--- a/public/app/plugins/panel/table/module.ts
+++ b/public/app/plugins/panel/table/module.ts
@@ -80,7 +80,6 @@ class TablePanelCtrl extends MetricsPanelCtrl {
this.pageIndex = 0;
if (this.panel.transform === 'annotations') {
- this.setTimeQueryStart();
return this.annotationsSrv
.getAnnotations({
dashboard: this.dashboard,
diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts
index ccaf3ddf423..90479a67602 100644
--- a/public/app/plugins/panel/table/renderer.ts
+++ b/public/app/plugins/panel/table/renderer.ts
@@ -1,8 +1,7 @@
import _ from 'lodash';
import moment from 'moment';
import kbn from 'app/core/utils/kbn';
-import { getColorFromHexRgbOrName } from '@grafana/ui';
-import { GrafanaTheme } from '@grafana/ui';
+import { GrafanaTheme, getValueFormat, getColorFromHexRgbOrName } from '@grafana/ui';
export class TableRenderer {
formatters: any[];
@@ -170,7 +169,7 @@ export class TableRenderer {
}
if (column.style.type === 'number') {
- const valueFormatter = kbn.valueFormats[column.unit || column.style.unit];
+ const valueFormatter = getValueFormat(column.unit || column.style.unit);
return v => {
if (v === null || v === void 0) {
diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts
index 4e4dd8121cf..70bdf49e5e4 100644
--- a/public/app/routes/GrafanaCtrl.ts
+++ b/public/app/routes/GrafanaCtrl.ts
@@ -8,7 +8,7 @@ import coreModule from 'app/core/core_module';
import { profiler } from 'app/core/profiler';
import appEvents from 'app/core/app_events';
import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv';
-import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv';
+import { TimeSrv, setTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader';
import { configureStore } from 'app/store/configureStore';
diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx
index 807608e6960..2cad3d828bf 100644
--- a/public/app/routes/ReactContainer.tsx
+++ b/public/app/routes/ReactContainer.tsx
@@ -4,8 +4,6 @@ import { Provider } from 'react-redux';
import coreModule from 'app/core/core_module';
import { store } from 'app/store/store';
-import { BackendSrv } from 'app/core/services/backend_srv';
-import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { ContextSrv } from 'app/core/services/context_srv';
function WrapInProvider(store, Component, props) {
@@ -20,8 +18,8 @@ function WrapInProvider(store, Component, props) {
export function reactContainer(
$route,
$location,
- backendSrv: BackendSrv,
- datasourceSrv: DatasourceSrv,
+ $injector,
+ $rootScope,
contextSrv: ContextSrv
) {
return {
@@ -43,9 +41,9 @@ export function reactContainer(
}
const props = {
- backendSrv: backendSrv,
- datasourceSrv: datasourceSrv,
- routeParams: $route.current.params,
+ $injector: $injector,
+ $rootScope: $rootScope,
+ $scope: scope,
};
ReactDOM.render(WrapInProvider(store, component, props), elem[0]);
diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts
index e7381740435..0f4c09a9c77 100644
--- a/public/app/routes/routes.ts
+++ b/public/app/routes/routes.ts
@@ -19,6 +19,7 @@ import UsersListPage from 'app/features/users/UsersListPage';
import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards';
import DataSourceSettingsPage from '../features/datasources/settings/DataSourceSettingsPage';
import OrgDetailsPage from '../features/org/OrgDetailsPage';
+import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage';
import config from 'app/core/config';
/** @ngInject */
@@ -51,16 +52,18 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
pageClass: 'page-dashboard',
})
.when('/d-solo/:uid/:slug', {
- templateUrl: 'public/app/features/panel/partials/soloPanel.html',
- controller: 'SoloPanelCtrl',
- reloadOnSearch: false,
- pageClass: 'page-dashboard',
+ template: ' ',
+ pageClass: 'dashboard-solo',
+ resolve: {
+ component: () => SoloPanelPage,
+ },
})
.when('/dashboard-solo/:type/:slug', {
- templateUrl: 'public/app/features/panel/partials/soloPanel.html',
- controller: 'SoloPanelCtrl',
- reloadOnSearch: false,
- pageClass: 'page-dashboard',
+ template: ' ',
+ pageClass: 'dashboard-solo',
+ resolve: {
+ component: () => SoloPanelPage,
+ },
})
.when('/dashboard/new', {
templateUrl: 'public/app/partials/dashboard.html',
diff --git a/public/app/types/alerting.ts b/public/app/types/alerting.ts
index 3a696d3a8d6..c987c2d38ed 100644
--- a/public/app/types/alerting.ts
+++ b/public/app/types/alerting.ts
@@ -32,4 +32,5 @@ export interface AlertRule {
export interface AlertRulesState {
items: AlertRule[];
searchQuery: string;
+ isLoading: boolean;
}
diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts
index 729760b41ea..da81a3023b7 100644
--- a/public/app/types/datasources.ts
+++ b/public/app/types/datasources.ts
@@ -12,4 +12,5 @@ export interface DataSourcesState {
dataSource: DataSourceSettings;
dataSourceMeta: Plugin;
hasFetched: boolean;
+ isLoadingDataSources: boolean;
}
diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts
index cd608dc11e3..51c3b7b0476 100644
--- a/public/app/types/plugins.ts
+++ b/public/app/types/plugins.ts
@@ -47,6 +47,7 @@ export interface PluginsState {
layoutMode: string;
hasFetched: boolean;
dashboards: PluginDashboard[];
+ isLoadingPluginDashboards: boolean;
}
export interface VariableQueryProps {
diff --git a/public/app/types/store.ts b/public/app/types/store.ts
index 0f5ad8cd088..78832052e96 100644
--- a/public/app/types/store.ts
+++ b/public/app/types/store.ts
@@ -9,6 +9,7 @@ import { ExploreState } from './explore';
import { UsersState, UserState } from './user';
import { OrganizationState } from './organization';
import { AppNotificationsState } from './appNotifications';
+import { PluginsState } from './plugins';
export interface StoreState {
navIndex: NavIndex;
@@ -24,4 +25,5 @@ export interface StoreState {
organization: OrganizationState;
appNotifications: AppNotificationsState;
user: UserState;
+ plugins: PluginsState;
}
diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss
index da6328b3c11..7b0ed869bdc 100644
--- a/public/sass/_variables.dark.scss
+++ b/public/sass/_variables.dark.scss
@@ -307,7 +307,8 @@ $tooltipArrowWidth: 5px;
$tooltipLinkColor: $link-color;
$graph-tooltip-bg: $dark-1;
-$tooltipBackground: $popover-help-bg;
+$tooltipBackground: $black;
+$tooltipColor: $gray-4;
$tooltipArrowColor: $tooltipBackground;
$tooltipBackgroundError: $brand-danger;
$tooltipBackgroundBrand: $brand-primary;
@@ -391,8 +392,8 @@ $panel-editor-tabs-line-color: #e3e3e3;
$panel-editor-viz-item-bg-hover: darken($blue, 47%);
$panel-editor-viz-item-bg-hover-active: darken($orange, 45%);
-$panel-options-group-border: 1px solid $dark-3;
-$panel-options-group-header-bg: linear-gradient(0deg, $gray-blue, $dark-1);
+$panel-options-group-border: none;
+$panel-options-group-header-bg: $gray-blue;
$panel-grid-placeholder-bg: darken($blue, 47%);
$panel-grid-placeholder-shadow: 0 0 4px $blue;
diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss
index cca183233be..10c074e1481 100644
--- a/public/sass/_variables.light.scss
+++ b/public/sass/_variables.light.scss
@@ -311,7 +311,8 @@ $tooltipArrowWidth: 5px;
$tooltipLinkColor: lighten($popover-help-color, 5%);
$graph-tooltip-bg: $gray-5;
-$tooltipBackground: $popover-help-bg;
+$tooltipBackground: $gray-1;
+$tooltipColor: $gray-7;
$tooltipArrowColor: $tooltipBackground; // Used by Angular tooltip
$tooltipBackgroundError: $brand-danger;
$tooltipBackgroundBrand: $brand-primary;
@@ -399,8 +400,8 @@ $panel-editor-tabs-line-color: $dark-5;
$panel-editor-viz-item-bg-hover: lighten($blue, 62%);
$panel-editor-viz-item-bg-hover-active: lighten($orange, 34%);
-$panel-options-group-border: 1px solid $gray-6;
-$panel-options-group-header-bg: linear-gradient(0deg, $gray-6, $gray-7);
+$panel-options-group-border: none;
+$panel-options-group-header-bg: $gray-5;
$panel-grid-placeholder-bg: lighten($blue, 62%);
$panel-grid-placeholder-shadow: 0 0 4px $blue-light;
diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss
index a10fefadf10..4e9e69c4d2f 100644
--- a/public/sass/_variables.scss
+++ b/public/sass/_variables.scss
@@ -135,7 +135,7 @@ $input-padding-y-sm: 4px !default;
$input-padding-x-lg: 20px !default;
$input-padding-y-lg: 10px !default;
-$input-height: (($font-size-base * $line-height-base) + ($input-padding-y * 2)) !default;
+$input-height: 35px !default;
$gf-form-margin: 0.2rem;
$gf-form-input-height: 35px;
diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss
index 3105e98fe35..a60259ac0f2 100644
--- a/public/sass/base/_icons.scss
+++ b/public/sass/base/_icons.scss
@@ -12,7 +12,7 @@
}
.gicon--has-hover {
- opacity: 0.5;
+ opacity: 0.7;
&:hover {
opacity: 1;
diff --git a/public/sass/components/_gf-form.scss b/public/sass/components/_gf-form.scss
index e86b901bb79..3c598de8852 100644
--- a/public/sass/components/_gf-form.scss
+++ b/public/sass/components/_gf-form.scss
@@ -5,7 +5,7 @@ $input-border: 1px solid $input-border-color;
margin-bottom: $gf-form-margin;
display: flex;
flex-direction: row;
- align-items: center;
+ align-items: flex-start;
text-align: left;
position: relative;
@@ -129,6 +129,11 @@ $input-border: 1px solid $input-border-color;
&:hover {
background: $list-item-hover-bg;
+ color: $link-color;
+ }
+
+ .fa-caret-down {
+ padding-left: 4px;
}
}
@@ -137,6 +142,7 @@ $input-border: 1px solid $input-border-color;
}
}
+
.gf-form-label + .gf-form-label {
margin-right: $gf-form-margin;
}
@@ -384,6 +390,10 @@ $input-border: 1px solid $input-border-color;
&--no-padding {
padding-left: 0;
}
+
+ &:hover {
+ color: $text-color;
+ }
}
select.gf-form-input ~ .gf-form-help-icon {
diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss
index 200f102d69a..7608f071e78 100644
--- a/public/sass/components/_slate_editor.scss
+++ b/public/sass/components/_slate_editor.scss
@@ -8,7 +8,8 @@
.slate-query-field__wrapper {
position: relative;
display: inline-block;
- padding: 6px 7px 4px;
+ padding: $input-padding-y $input-padding-x;
+ min-height: $gf-form-input-height;
width: 100%;
cursor: text;
line-height: $line-height-base;
diff --git a/public/sass/components/_submenu.scss b/public/sass/components/_submenu.scss
index d4be5264d9d..6c450f7e6dd 100644
--- a/public/sass/components/_submenu.scss
+++ b/public/sass/components/_submenu.scss
@@ -40,9 +40,9 @@
background-color: $input-bg;
border: 1px solid $input-border-color;
border-radius: $input-border-radius;
- box-sizing: content-box;
display: inline-block;
color: $text-color;
+ height: $gf-form-input-height;
.label-tag {
margin: 0 5px;
diff --git a/public/sass/components/_toggle_button_group.scss b/public/sass/components/_toggle_button_group.scss
index ae1bb621d16..eb57860233d 100644
--- a/public/sass/components/_toggle_button_group.scss
+++ b/public/sass/components/_toggle_button_group.scss
@@ -1,5 +1,6 @@
.toggle-button-group {
display: flex;
+ padding-top: 5px;
.btn {
@include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow);
diff --git a/public/sass/components/_tooltip.scss b/public/sass/components/_tooltip.scss
index 650d8ba1b1a..6801f196d59 100644
--- a/public/sass/components/_tooltip.scss
+++ b/public/sass/components/_tooltip.scss
@@ -8,8 +8,8 @@
z-index: $zindex-tooltip;
display: block;
visibility: visible;
- font-size: 11px;
line-height: 1.4;
+ font-weight: 500;
@include opacity(0);
&.in {
@@ -40,7 +40,8 @@
// Wrapper for the tooltip content
.tooltip-inner {
max-width: 400px;
- padding: 8px;
+ padding: 8px 16px;
+ padding: 4px 8px;
color: $tooltipColor;
text-align: center;
text-decoration: none;
diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss
index b92bd596193..518d5d0f446 100644
--- a/public/sass/components/_view_states.scss
+++ b/public/sass/components/_view_states.scss
@@ -50,3 +50,4 @@
display: none;
}
}
+
diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss
index a0ff9fd877c..9ca4e092f02 100644
--- a/public/sass/pages/_dashboard.scss
+++ b/public/sass/pages/_dashboard.scss
@@ -17,6 +17,13 @@ div.flot-text {
height: 100%;
}
+.dashboard-solo {
+ .footer,
+ .sidemenu {
+ display: none;
+ }
+}
+
.panel-solo {
position: fixed;
bottom: 0;
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss
index b1288ccade0..151130a69e2 100644
--- a/public/sass/pages/_explore.scss
+++ b/public/sass/pages/_explore.scss
@@ -322,6 +322,7 @@
.query-row {
display: flex;
position: relative;
+ align-items: flex-start;
& + & {
margin-top: 0.5rem;
@@ -335,17 +336,17 @@
.query-row-status {
position: absolute;
top: 0;
- right: 90px;
+ right: 105px;
z-index: 1015;
display: flex;
flex-direction: column;
justify-content: center;
- height: 34px;
+ height: $input-height;
}
.query-row-field {
margin-right: 3px;
- width: 100%;
+ flex-grow: 1;
}
.query-transactions {
@@ -381,20 +382,14 @@
// Prometheus-specifics, to be extracted to datasource soon
-.prom-query-field {
- display: flex;
-}
+.explore {
+ .prom-query-field-info {
+ margin: 0.25em 0.5em 0.5em;
+ display: flex;
-.prom-query-field-wrapper {
- width: 100%;
-}
-
-.prom-query-field-info {
- margin: 0.25em 0.5em 0.5em;
- display: flex;
-
- details {
- margin-left: 1em;
+ details {
+ margin-left: 1em;
+ }
}
}
diff --git a/public/test/core/redux/reducerTester.test.ts b/public/test/core/redux/reducerTester.test.ts
new file mode 100644
index 00000000000..63d163647fc
--- /dev/null
+++ b/public/test/core/redux/reducerTester.test.ts
@@ -0,0 +1,56 @@
+import { reducerFactory, actionCreatorFactory } from 'app/core/redux';
+import { reducerTester } from './reducerTester';
+
+interface DummyState {
+ data: string[];
+}
+
+const initialState: DummyState = {
+ data: [],
+};
+
+const dummyAction = actionCreatorFactory('dummyAction').create();
+
+const mutatingReducer = reducerFactory(initialState)
+ .addMapper({
+ filter: dummyAction,
+ mapper: (state, action) => {
+ state.data.push(action.payload);
+ return state;
+ },
+ })
+ .create();
+
+const okReducer = reducerFactory(initialState)
+ .addMapper({
+ filter: dummyAction,
+ mapper: (state, action) => {
+ return {
+ ...state,
+ data: state.data.concat(action.payload),
+ };
+ },
+ })
+ .create();
+
+describe('reducerTester', () => {
+ describe('when reducer mutates state', () => {
+ it('then it should throw', () => {
+ expect(() => {
+ reducerTester()
+ .givenReducer(mutatingReducer, initialState)
+ .whenActionIsDispatched(dummyAction('some string'));
+ }).toThrow();
+ });
+ });
+
+ describe('when reducer does not mutate state', () => {
+ it('then it should not throw', () => {
+ expect(() => {
+ reducerTester()
+ .givenReducer(okReducer, initialState)
+ .whenActionIsDispatched(dummyAction('some string'));
+ }).not.toThrow();
+ });
+ });
+});
diff --git a/public/test/core/redux/reducerTester.ts b/public/test/core/redux/reducerTester.ts
new file mode 100644
index 00000000000..f8d8c921767
--- /dev/null
+++ b/public/test/core/redux/reducerTester.ts
@@ -0,0 +1,79 @@
+import { Reducer } from 'redux';
+
+import { ActionOf } from 'app/core/redux/actionCreatorFactory';
+
+export interface Given {
+ givenReducer: (reducer: Reducer>, state: State) => When;
+}
+
+export interface When {
+ whenActionIsDispatched: (action: ActionOf) => Then;
+}
+
+export interface Then {
+ thenStateShouldEqual: (state: State) => Then;
+}
+
+interface ObjectType extends Object {
+ [key: string]: any;
+}
+
+const deepFreeze = (obj: T): T => {
+ Object.freeze(obj);
+
+ const isNotException = (object: any, propertyName: any) =>
+ typeof object === 'function'
+ ? propertyName !== 'caller' && propertyName !== 'callee' && propertyName !== 'arguments'
+ : true;
+ const hasOwnProp = Object.prototype.hasOwnProperty;
+
+ if (obj && obj instanceof Object) {
+ const object: ObjectType = obj;
+ Object.getOwnPropertyNames(object).forEach(propertyName => {
+ const objectProperty: any = object[propertyName];
+ if (
+ hasOwnProp.call(object, propertyName) &&
+ isNotException(object, propertyName) &&
+ objectProperty &&
+ (typeof objectProperty === 'object' || typeof objectProperty === 'function') &&
+ Object.isFrozen(objectProperty) === false
+ ) {
+ deepFreeze(objectProperty);
+ }
+ });
+ }
+
+ return obj;
+};
+
+interface ReducerTester extends Given, When, Then {}
+
+export const reducerTester = (): Given => {
+ let reducerUnderTest: Reducer> = null;
+ let resultingState: State = null;
+ let initialState: State = null;
+
+ const givenReducer = (reducer: Reducer>, state: State): When => {
+ reducerUnderTest = reducer;
+ initialState = { ...state };
+ initialState = deepFreeze(initialState);
+
+ return instance;
+ };
+
+ const whenActionIsDispatched = (action: ActionOf): Then => {
+ resultingState = reducerUnderTest(initialState, action);
+
+ return instance;
+ };
+
+ const thenStateShouldEqual = (state: State): Then => {
+ expect(state).toEqual(resultingState);
+
+ return instance;
+ };
+
+ const instance: ReducerTester = { thenStateShouldEqual, givenReducer, whenActionIsDispatched };
+
+ return instance;
+};
diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts
index 0345576d96c..1570c7dd9b7 100644
--- a/public/test/specs/helpers.ts
+++ b/public/test/specs/helpers.ts
@@ -2,7 +2,7 @@ import _ from 'lodash';
import config from 'app/core/config';
import * as dateMath from 'app/core/utils/datemath';
import { angularMocks, sinon } from '../lib/common';
-import { PanelModel } from 'app/features/dashboard/panel_model';
+import { PanelModel } from 'app/features/dashboard/state/PanelModel';
export function ControllerTestContext(this: any) {
const self = this;
@@ -182,7 +182,7 @@ export function TemplateSrvStub(this: any) {
return [];
};
this.fillVariableValuesForUrl = () => {};
- this.updateTemplateData = () => {};
+ this.updateIndex = () => {};
this.variableExists = () => {
return false;
};
diff --git a/scripts/build/publish.sh b/scripts/build/publish.sh
index 264d930e51b..785f46f22a0 100755
--- a/scripts/build/publish.sh
+++ b/scripts/build/publish.sh
@@ -6,8 +6,8 @@ EXTRA_OPTS="$@"
# Right now we hack this in into the publish script.
# Eventually we might want to keep a list of all previous releases somewhere.
-_releaseNoteUrl="https://community.grafana.com/t/release-notes-v5-4-x/12215"
-_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v5-4/"
+_releaseNoteUrl="https://community.grafana.com/t/release-notes-v6-0-x/14010"
+_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v6-0/"
./scripts/build/release_publisher/release_publisher \
--wn ${_whatsNewUrl} \
diff --git a/scripts/grunt/options/exec.js b/scripts/grunt/options/exec.js
index 27bfd7ae43d..3296abecfd4 100644
--- a/scripts/grunt/options/exec.js
+++ b/scripts/grunt/options/exec.js
@@ -1,4 +1,4 @@
-module.exports = function (config, grunt) {
+module.exports = function(config, grunt) {
'use strict';
return {
@@ -19,6 +19,7 @@ module.exports = function (config, grunt) {
src: ['public/app/**/*.ts*'],
},
jest: 'node ./node_modules/jest-cli/bin/jest.js --maxWorkers 2',
- webpack: 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js',
+ webpack:
+ 'node ./node_modules/webpack/bin/webpack.js --config scripts/webpack/webpack.prod.js',
};
};
diff --git a/scripts/webpack/loaders/blobUrl.js b/scripts/webpack/loaders/blobUrl.js
new file mode 100644
index 00000000000..951cf3f5cc7
--- /dev/null
+++ b/scripts/webpack/loaders/blobUrl.js
@@ -0,0 +1,6 @@
+const loaderUtils = require('loader-utils');
+
+module.exports = function blobUrl(source) {
+ const { type } = loaderUtils.getOptions(this) || {};
+ return `module.exports = URL.createObjectURL(new Blob([${JSON.stringify(source)}]${type ? `, { type: ${JSON.stringify(type)} }` : ''}));`;
+};
diff --git a/scripts/webpack/loaders/compile.js b/scripts/webpack/loaders/compile.js
new file mode 100644
index 00000000000..1716fd00f73
--- /dev/null
+++ b/scripts/webpack/loaders/compile.js
@@ -0,0 +1,87 @@
+const loaderUtils = require('loader-utils');
+
+const WebWorkerTemplatePlugin = require('webpack/lib/webworker/WebWorkerTemplatePlugin');
+const ExternalsPlugin = require('webpack/lib/ExternalsPlugin');
+const NodeTargetPlugin = require('webpack/lib/node/NodeTargetPlugin');
+const LoaderTargetPlugin = require('webpack/lib/LoaderTargetPlugin');
+const SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin');
+
+const COMPILATION_METADATA = Symbol('COMPILATION_METADATA');
+
+module.exports.COMPILATION_METADATA = COMPILATION_METADATA;
+
+module.exports.pitch = function pitch(remainingRequest) {
+ const { target, plugins = [], output, emit } = loaderUtils.getOptions(this) || {};
+
+ if (target !== 'worker') {
+ throw new Error(`Unsupported compile target: ${JSON.stringify(target)}`);
+ }
+
+ this.cacheable(false);
+
+ const { filename, options = {} } = getOutputFilename(output, { target });
+
+ // eslint-disable-next-line no-underscore-dangle
+ const currentCompilation = this._compilation;
+
+ const outputFilename = loaderUtils.interpolateName(this, filename, {
+ context: options.context || currentCompilation.options.context,
+ regExp: options.regExp,
+ });
+
+ const outputOptions = {
+ filename: outputFilename,
+ chunkFilename: `${outputFilename}.[id]`,
+ namedChunkFilename: null,
+ };
+
+ const compilerOptions = currentCompilation.compiler.options;
+ const childCompiler = currentCompilation.createChildCompiler('worker', outputOptions, [
+ // https://github.com/webpack/webpack/blob/master/lib/WebpackOptionsApply.js
+ new WebWorkerTemplatePlugin(outputOptions),
+ new LoaderTargetPlugin('webworker'),
+ ...((this.target === 'web') || (this.target === 'webworker') ? [] : [new NodeTargetPlugin()]),
+
+ // https://github.com/webpack-contrib/worker-loader/issues/95#issuecomment-352856617
+ ...(compilerOptions.externals ? [new ExternalsPlugin(compilerOptions.externals)] : []),
+
+ ...plugins,
+
+ new SingleEntryPlugin(this.context, `!!${remainingRequest}`, 'main'),
+ ]);
+
+ const subCache = `subcache ${__dirname} ${remainingRequest}`;
+
+ childCompiler.plugin('compilation', (compilation) => {
+ if (!compilation.cache) { return; }
+ if (!(subCache in compilation.cache)) { Object.assign(compilation.cache, { [subCache]: {} }); }
+ Object.assign(compilation, { cache: compilation.cache[subCache] });
+ });
+
+ const callback = this.async();
+
+ childCompiler.runAsChild((error, entries, compilation) => {
+ if (error) { return callback(error); }
+ if (entries.length === 0) { return callback(null, null); }
+ const mainFilename = entries[0].files[0];
+ if (emit === false) { delete currentCompilation.assets[mainFilename]; }
+ callback(null, compilation.assets[mainFilename].source(), null, {
+ [COMPILATION_METADATA]: entries[0].files,
+ });
+ });
+};
+
+function getOutputFilename(options, { target }) {
+ if (!options) { return { filename: `[hash].${target}.js`, options: undefined }; }
+ if (typeof options === 'string') { return { filename: options, options: undefined }; }
+ if (typeof options === 'object') {
+ return {
+ filename: options.filename,
+ options: {
+ context: options.context,
+ regExp: options.regExp,
+ },
+ };
+ }
+ throw new Error(`Invalid compile output options: ${options}`);
+}
diff --git a/yarn.lock b/yarn.lock
index c674f3e1709..169abd40ee4 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1773,6 +1773,13 @@
dependencies:
"@types/react" "*"
+"@types/react-grid-layout@^0.16.6":
+ version "0.16.6"
+ resolved "https://registry.yarnpkg.com/@types/react-grid-layout/-/react-grid-layout-0.16.6.tgz#9149efe128e05d59c54063c7781d18b8febe112c"
+ integrity sha512-Jp0VfCHJE4uxekPBPpRkADKOjoSHssF2ba1ZMMAfCEqkoSkE+K+3bhI39++fbd7MqGySaqADVHeOoxlBnA3p5g==
+ dependencies:
+ "@types/react" "*"
+
"@types/react-select@^2.0.4":
version "2.0.11"
resolved "https://registry.yarnpkg.com/@types/react-select/-/react-select-2.0.11.tgz#9b2b1fdb12b67a5a617c5f572e15617636cc65af"
@@ -1796,6 +1803,14 @@
dependencies:
"@types/react" "*"
+"@types/react-virtualized@^9.18.12":
+ version "9.18.12"
+ resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.18.12.tgz#541e65c5e0b4629d6a1c6f339171c7943e016ecb"
+ integrity sha512-Msdpt9zvYlb5Ul4PA339QUkJ0/z2O+gaFxed1rG+2rZjbe6XdYo7jWfJe206KBnjj84DwPPIbPFQCtoGuNwNTQ==
+ dependencies:
+ "@types/prop-types" "*"
+ "@types/react" "*"
+
"@types/react@*", "@types/react@16.7.6", "@types/react@^16.7.6":
version "16.7.6"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.7.6.tgz#80e4bab0d0731ad3ae51f320c4b08bdca5f03040"
@@ -6077,7 +6092,7 @@ debug@^4.1.0:
dependencies:
ms "^2.1.1"
-debuglog@*, debuglog@^1.0.1:
+debuglog@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492"
integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=
@@ -8991,7 +9006,7 @@ import-local@^2.0.0:
pkg-dir "^3.0.0"
resolve-cwd "^2.0.0"
-imurmurhash@*, imurmurhash@^0.1.4:
+imurmurhash@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea"
integrity sha1-khi5srkoojixPcT7a21XbyMUU+o=
@@ -10763,11 +10778,6 @@ lockfile@^1.0.4:
dependencies:
signal-exit "^3.0.2"
-lodash._baseindexof@*:
- version "3.1.0"
- resolved "https://registry.yarnpkg.com/lodash._baseindexof/-/lodash._baseindexof-3.1.0.tgz#fe52b53a1c6761e42618d654e4a25789ed61822c"
- integrity sha1-/lK1OhxnYeQmGNZU5KJXie1hgiw=
-
lodash._baseuniq@~4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8"
@@ -10776,29 +10786,12 @@ lodash._baseuniq@~4.6.0:
lodash._createset "~4.0.0"
lodash._root "~3.0.0"
-lodash._bindcallback@*:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/lodash._bindcallback/-/lodash._bindcallback-3.0.1.tgz#e531c27644cf8b57a99e17ed95b35c748789392e"
- integrity sha1-5THCdkTPi1epnhftlbNcdIeJOS4=
-
-lodash._cacheindexof@*:
- version "3.0.2"
- resolved "https://registry.yarnpkg.com/lodash._cacheindexof/-/lodash._cacheindexof-3.0.2.tgz#3dc69ac82498d2ee5e3ce56091bafd2adc7bde92"
- integrity sha1-PcaayCSY0u5ePOVgkbr9Ktx73pI=
-
-lodash._createcache@*:
- version "3.1.2"
- resolved "https://registry.yarnpkg.com/lodash._createcache/-/lodash._createcache-3.1.2.tgz#56d6a064017625e79ebca6b8018e17440bdcf093"
- integrity sha1-VtagZAF2JeeevKa4AY4XRAvc8JM=
- dependencies:
- lodash._getnative "^3.0.0"
-
lodash._createset@~4.0.0:
version "4.0.3"
resolved "https://registry.yarnpkg.com/lodash._createset/-/lodash._createset-4.0.3.tgz#0f4659fbb09d75194fa9e2b88a6644d363c9fe26"
integrity sha1-D0ZZ+7CddRlPqeK4imZE02PJ/iY=
-lodash._getnative@*, lodash._getnative@^3.0.0:
+lodash._getnative@^3.0.0:
version "3.9.1"
resolved "https://registry.yarnpkg.com/lodash._getnative/-/lodash._getnative-3.9.1.tgz#570bc7dede46d61cdcde687d65d3eecbaa3aaff5"
integrity sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=
@@ -10892,11 +10885,6 @@ lodash.mergewith@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz#639057e726c3afbdb3e7d42741caa8d6e4335927"
integrity sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==
-lodash.restparam@*:
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
- integrity sha1-k2pOMJ7zMKdkXtQUWYbIWuWyCAU=
-
lodash.some@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"
@@ -11562,6 +11550,11 @@ moment@^2.22.2:
resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b"
integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==
+monaco-editor@^0.15.6:
+ version "0.15.6"
+ resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.15.6.tgz#d63b3b06f86f803464f003b252627c3eb4a09483"
+ integrity sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg==
+
moo@^0.4.3:
version "0.4.3"
resolved "https://registry.yarnpkg.com/moo/-/moo-0.4.3.tgz#3f847a26f31cf625a956a87f2b10fbc013bfd10e"
@@ -14463,7 +14456,7 @@ readable-stream@~1.1.10:
isarray "0.0.1"
string_decoder "~0.10.x"
-readdir-scoped-modules@*, readdir-scoped-modules@^1.0.0:
+readdir-scoped-modules@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz#9fafa37d286be5d92cbaebdee030dc9b5f406747"
integrity sha1-n6+jfShr5dksuuve4DDcm19AZ0c=
@@ -14655,6 +14648,13 @@ regex-not@^1.0.0, regex-not@^1.0.2:
extend-shallow "^3.0.2"
safe-regex "^1.1.0"
+regexp-replace-loader@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/regexp-replace-loader/-/regexp-replace-loader-1.0.1.tgz#5dae73be9ee82a4d94d0955c2fa3fc923e134d7e"
+ integrity sha1-Xa5zvp7oKk2U0JVcL6P8kj4TTX4=
+ dependencies:
+ loader-utils "^1.0.2"
+
regexp-tree@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/regexp-tree/-/regexp-tree-0.1.0.tgz#a56ad7746097888ea16457479029ec9345b96ab0"