mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AzureMonitor: Frontend cleanup (#66871)
* Remove unused mocks * Remove time grain converter anys * Improve mocks - Add context mock - Update datasource mock - Add util functions * Remove anys from log_analytics_test * Improve response typing * Remove redundant angular code * Remove more anys - Add Resource type * More type updates * Remove unused code and update arg ds test * Remove old annotations test * Remove unused code and update some more types * Fix lint * Fix lint
This commit is contained in:
@@ -3630,70 +3630,13 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/__mocks__/query_ctrl.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/__mocks__/schema.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/azure_log_analytics_datasource.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_log_analytics/response_parser.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "8"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "9"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "10"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "14"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_monitor/azure_monitor_datasource.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_monitor/response_parser.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/azure_resource_graph/azure_resource_graph_datasource.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/components/LogsQueryEditor/QueryField.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "2"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/components/MonitorConfig.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
@@ -3701,28 +3644,6 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/datasource/azuremonitor/components/QueryEditor/QueryEditor.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/datasource.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/log_analytics/querystring_builder.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/time_grain_converter.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/types/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/utils/common.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
],
|
||||
"public/app/plugins/datasource/azuremonitor/utils/messageFromError.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
import { ContextSrv } from 'app/core/services/context_srv';
|
||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import Datasource from '../datasource';
|
||||
import { AzureDataSourceJsonData } from '../types';
|
||||
|
||||
import { createMockInstanceSetttings } from './instanceSettings';
|
||||
import { DeepPartial } from './utils';
|
||||
|
||||
export interface Context {
|
||||
instanceSettings: DataSourceInstanceSettings<AzureDataSourceJsonData>;
|
||||
templateSrv: TemplateSrv;
|
||||
datasource: Datasource;
|
||||
getResource: jest.Mock;
|
||||
}
|
||||
|
||||
export function createContext(overrides?: DeepPartial<Context>): Context {
|
||||
const instanceSettings = createMockInstanceSetttings(overrides?.instanceSettings);
|
||||
return {
|
||||
instanceSettings,
|
||||
templateSrv: new TemplateSrv(),
|
||||
datasource: new Datasource(instanceSettings),
|
||||
getResource: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
type DeepPartial<T> = {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
};
|
||||
const contextSrv = new ContextSrv();
|
||||
const timeSrv = new TimeSrv(contextSrv);
|
||||
|
||||
|
||||
@@ -1,29 +1,71 @@
|
||||
import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings, PluginType } from '@grafana/data';
|
||||
|
||||
import { AzureDataSourceInstanceSettings, AzureDataSourceJsonData } from '../types';
|
||||
import { AzureDataSourceInstanceSettings } from '../types';
|
||||
|
||||
import { DeepPartial, mapPartialArrayObject } from './utils';
|
||||
|
||||
export const createMockInstanceSetttings = (
|
||||
overrides?: Partial<DataSourceInstanceSettings>,
|
||||
jsonDataOverrides?: Partial<AzureDataSourceJsonData>
|
||||
): AzureDataSourceInstanceSettings => ({
|
||||
url: '/ds/1',
|
||||
id: 1,
|
||||
uid: 'abc',
|
||||
type: 'azuremonitor',
|
||||
access: 'proxy',
|
||||
meta: {} as DataSourcePluginMeta,
|
||||
name: 'azure',
|
||||
readOnly: false,
|
||||
...overrides,
|
||||
overrides?: DeepPartial<DataSourceInstanceSettings>
|
||||
): AzureDataSourceInstanceSettings => {
|
||||
const metaOverrides = overrides?.meta;
|
||||
return {
|
||||
url: '/ds/1',
|
||||
id: 1,
|
||||
uid: 'abc',
|
||||
type: 'azuremonitor',
|
||||
access: 'proxy',
|
||||
name: 'azure',
|
||||
readOnly: false,
|
||||
...overrides,
|
||||
meta: {
|
||||
id: 'grafana-azure-monitor-datasource',
|
||||
name: 'Azure Monitor',
|
||||
type: PluginType.datasource,
|
||||
module: 'path_to_module',
|
||||
baseUrl: 'base_url',
|
||||
...metaOverrides,
|
||||
info: {
|
||||
description: 'Azure Monitor',
|
||||
updated: 'updated',
|
||||
version: '1.0.0',
|
||||
...metaOverrides?.info,
|
||||
screenshots: mapPartialArrayObject(
|
||||
{ name: 'Azure Screenshot', path: 'path_to_screenshot' },
|
||||
metaOverrides?.info?.screenshots
|
||||
),
|
||||
links: mapPartialArrayObject(
|
||||
{ name: 'Azure Link', url: 'link_url', target: '_blank' },
|
||||
metaOverrides?.info?.links
|
||||
),
|
||||
author: {
|
||||
name: 'test',
|
||||
...metaOverrides?.info?.author,
|
||||
},
|
||||
logos: {
|
||||
large: 'large.logo',
|
||||
small: 'small.logo',
|
||||
...metaOverrides?.info?.logos,
|
||||
},
|
||||
build: {
|
||||
time: 0,
|
||||
repo: 'repo',
|
||||
branch: 'branch',
|
||||
hash: 'hash',
|
||||
number: 1,
|
||||
pr: 1,
|
||||
...metaOverrides?.info?.build,
|
||||
},
|
||||
},
|
||||
},
|
||||
jsonData: {
|
||||
cloudName: 'azuremonitor',
|
||||
azureAuthType: 'clientsecret',
|
||||
|
||||
jsonData: {
|
||||
cloudName: 'azuremonitor',
|
||||
azureAuthType: 'clientsecret',
|
||||
|
||||
// monitor
|
||||
tenantId: 'abc-123',
|
||||
clientId: 'def-456',
|
||||
subscriptionId: 'ghi-789',
|
||||
...jsonDataOverrides,
|
||||
},
|
||||
});
|
||||
// monitor
|
||||
tenantId: 'abc-123',
|
||||
clientId: 'def-456',
|
||||
subscriptionId: 'ghi-789',
|
||||
...overrides?.jsonData,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
export class QueryCtrl {
|
||||
target: any;
|
||||
datasource: any;
|
||||
panelCtrl: any;
|
||||
panel: any;
|
||||
hasRawMode = false;
|
||||
error = '';
|
||||
|
||||
constructor(public $scope: any) {
|
||||
this.panelCtrl = this.panelCtrl || { panel: {} };
|
||||
this.target = this.target || { target: '' };
|
||||
this.panel = this.panelCtrl.panel;
|
||||
}
|
||||
|
||||
refresh() {}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import { QueryCtrl } from './query_ctrl';
|
||||
|
||||
export { QueryCtrl };
|
||||
@@ -38,3 +38,19 @@ export function createTemplateVariables(templateableProps: string[], value = '')
|
||||
});
|
||||
return templateVariables;
|
||||
}
|
||||
|
||||
export type DeepPartial<K> = {
|
||||
[attr in keyof K]?: K[attr] extends object ? DeepPartial<K[attr]> : K[attr];
|
||||
};
|
||||
|
||||
export function mapPartialArrayObject<T extends object>(defaultValue: T, arr?: Array<DeepPartial<T | undefined>>): T[] {
|
||||
if (!arr) {
|
||||
return [defaultValue];
|
||||
}
|
||||
return arr.map((item?: DeepPartial<T>) => {
|
||||
if (!item) {
|
||||
return defaultValue;
|
||||
}
|
||||
return { ...item, ...defaultValue };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ export default class FakeSchemaData {
|
||||
};
|
||||
}
|
||||
|
||||
static getlogAnalyticsFakeMetadata(): any {
|
||||
static getlogAnalyticsFakeMetadata() {
|
||||
return {
|
||||
tables: [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { toUtc } from '@grafana/data';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import { Context, createContext } from '../__mocks__/datasource';
|
||||
import createMockQuery from '../__mocks__/query';
|
||||
import { createTemplateVariables } from '../__mocks__/utils';
|
||||
import { singleVariable } from '../__mocks__/variables';
|
||||
@@ -18,39 +18,29 @@ jest.mock('@grafana/runtime', () => ({
|
||||
getTemplateSrv: () => templateSrv,
|
||||
}));
|
||||
|
||||
const makeResourceURI = (
|
||||
resourceName: string,
|
||||
resourceGroup = 'test-resource-group',
|
||||
subscriptionID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'
|
||||
) =>
|
||||
`/subscriptions/${subscriptionID}/resourceGroups/${resourceGroup}/providers/Microsoft.OperationalInsights/workspaces/${resourceName}`;
|
||||
|
||||
describe('AzureLogAnalyticsDatasource', () => {
|
||||
const ctx: any = {};
|
||||
let ctx: Context;
|
||||
|
||||
beforeEach(() => {
|
||||
templateSrv.init([singleVariable]);
|
||||
templateSrv.getVariables = jest.fn().mockReturnValue([singleVariable]);
|
||||
ctx.instanceSettings = {
|
||||
jsonData: { subscriptionId: 'xxx' },
|
||||
url: 'http://azureloganalyticsapi',
|
||||
templateSrv: templateSrv,
|
||||
};
|
||||
|
||||
ctx.ds = new AzureMonitorDatasource(ctx.instanceSettings);
|
||||
ctx = createContext({
|
||||
instanceSettings: { jsonData: { subscriptionId: 'xxx' }, url: 'http://azureloganalyticsapi' },
|
||||
});
|
||||
ctx.templateSrv = templateSrv;
|
||||
});
|
||||
|
||||
describe('When performing getSchema', () => {
|
||||
beforeEach(() => {
|
||||
ctx.mockGetResource = jest.fn().mockImplementation((path: string) => {
|
||||
ctx.getResource = jest.fn().mockImplementation((path: string) => {
|
||||
expect(path).toContain('metadata');
|
||||
return Promise.resolve(FakeSchemaData.getlogAnalyticsFakeMetadata());
|
||||
});
|
||||
ctx.ds.azureLogAnalyticsDatasource.getResource = ctx.mockGetResource;
|
||||
ctx.datasource.azureLogAnalyticsDatasource.getResource = ctx.getResource;
|
||||
});
|
||||
|
||||
it('should return a schema to use with monaco-kusto', async () => {
|
||||
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||
|
||||
expect(result.database.tables).toHaveLength(2);
|
||||
expect(result.database.tables[0].name).toBe('Alert');
|
||||
@@ -81,12 +71,12 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
});
|
||||
|
||||
it('should interpolate variables when making a request for a schema with a uri that contains template variables', async () => {
|
||||
await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace/$var1');
|
||||
expect(ctx.mockGetResource).lastCalledWith('loganalytics/v1myWorkspace/var1-foo/metadata');
|
||||
await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace/$var1');
|
||||
expect(ctx.getResource).lastCalledWith('loganalytics/v1myWorkspace/var1-foo/metadata');
|
||||
});
|
||||
|
||||
it('should include macros as suggested functions', async () => {
|
||||
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||
expect(result.database.functions.map((f: { name: string }) => f.name)).toEqual([
|
||||
'Func1',
|
||||
'_AzureBackup_GetVaults',
|
||||
@@ -99,123 +89,42 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
});
|
||||
|
||||
it('should include template variables as global parameters', async () => {
|
||||
const result = await ctx.ds.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||
const result = await ctx.datasource.azureLogAnalyticsDatasource.getKustoSchema('myWorkspace');
|
||||
expect(result.globalParameters.map((f: { name: string }) => f.name)).toEqual([`$${singleVariable.name}`]);
|
||||
});
|
||||
});
|
||||
|
||||
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',
|
||||
id: makeResourceURI('a-workspace'),
|
||||
properties: {
|
||||
source: 'Azure',
|
||||
customerId: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
let annotationResults: any[];
|
||||
|
||||
beforeEach(async () => {
|
||||
ctx.ds.azureLogAnalyticsDatasource.getResource = jest.fn().mockImplementation((path: string) => {
|
||||
if (path.indexOf('Microsoft.OperationalInsights/workspaces') > -1) {
|
||||
return Promise.resolve(workspaceResponse);
|
||||
} else {
|
||||
return Promise.resolve(tableResponse);
|
||||
}
|
||||
});
|
||||
|
||||
annotationResults = await ctx.ds.annotationQuery({
|
||||
annotation: {
|
||||
rawQuery: 'Heartbeat | where $__timeFilter()| project TimeGenerated, Text=Computer, tags="test"',
|
||||
workspace: 'abc1b44e-3e57-4410-b027-6cc0ae6dee67',
|
||||
},
|
||||
range: {
|
||||
from: toUtc('2017-08-22T20:00:00Z'),
|
||||
to: toUtc('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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getWorkspaces', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ds.azureLogAnalyticsDatasource.getWorkspaceList = jest
|
||||
ctx.datasource.azureLogAnalyticsDatasource.getResource = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ value: [{ name: 'foobar', id: 'foo', properties: { customerId: 'bar' } }] });
|
||||
});
|
||||
|
||||
it('should return the workspace id', async () => {
|
||||
const workspaces = await ctx.ds.azureLogAnalyticsDatasource.getWorkspaces('sub');
|
||||
const workspaces = await ctx.datasource.azureLogAnalyticsDatasource.getWorkspaces('sub');
|
||||
expect(workspaces).toEqual([{ text: 'foobar', value: 'foo' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getFirstWorkspace', () => {
|
||||
beforeEach(() => {
|
||||
ctx.ds.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription = jest.fn().mockResolvedValue('foo');
|
||||
ctx.ds.azureLogAnalyticsDatasource.getWorkspaces = jest
|
||||
ctx.datasource.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription = jest.fn().mockResolvedValue('foo');
|
||||
ctx.datasource.azureLogAnalyticsDatasource.getWorkspaces = jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ text: 'foobar', value: 'foo' }]);
|
||||
ctx.ds.azureLogAnalyticsDatasource.firstWorkspace = undefined;
|
||||
ctx.datasource.azureLogAnalyticsDatasource.firstWorkspace = undefined;
|
||||
});
|
||||
|
||||
it('should return the stored workspace', async () => {
|
||||
ctx.ds.azureLogAnalyticsDatasource.firstWorkspace = 'bar';
|
||||
const workspace = await ctx.ds.azureLogAnalyticsDatasource.getFirstWorkspace();
|
||||
ctx.datasource.azureLogAnalyticsDatasource.firstWorkspace = 'bar';
|
||||
const workspace = await ctx.datasource.azureLogAnalyticsDatasource.getFirstWorkspace();
|
||||
expect(workspace).toEqual('bar');
|
||||
expect(ctx.ds.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription).not.toHaveBeenCalled();
|
||||
expect(ctx.datasource.azureLogAnalyticsDatasource.getDefaultOrFirstSubscription).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return the first workspace', async () => {
|
||||
const workspace = await ctx.ds.azureLogAnalyticsDatasource.getFirstWorkspace();
|
||||
const workspace = await ctx.datasource.azureLogAnalyticsDatasource.getFirstWorkspace();
|
||||
expect(workspace).toEqual('foo');
|
||||
});
|
||||
});
|
||||
@@ -252,15 +161,9 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
});
|
||||
|
||||
describe('When performing filterQuery', () => {
|
||||
const ctx: any = {};
|
||||
let laDatasource: AzureLogAnalyticsDatasource;
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.instanceSettings = {
|
||||
jsonData: { subscriptionId: 'xxx' },
|
||||
url: 'http://azureloganalyticsapi',
|
||||
};
|
||||
|
||||
laDatasource = new AzureLogAnalyticsDatasource(ctx.instanceSettings);
|
||||
});
|
||||
|
||||
@@ -351,7 +254,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
it('should return a query unchanged if no template variables are provided', () => {
|
||||
const query = createMockQuery();
|
||||
query.queryType = AzureQueryType.LogAnalytics;
|
||||
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
|
||||
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
|
||||
expect(templatedQuery[0]).toEqual(query);
|
||||
});
|
||||
|
||||
@@ -369,7 +272,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
...query.azureLogAnalytics,
|
||||
...azureLogAnalytics,
|
||||
};
|
||||
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
|
||||
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
|
||||
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||
expect(templatedQuery[0].azureLogAnalytics).toMatchObject({
|
||||
query: templateVariables.get('query')?.templateVariable.current.value,
|
||||
@@ -389,7 +292,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
...query.azureLogAnalytics,
|
||||
...azureLogAnalytics,
|
||||
};
|
||||
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
|
||||
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
|
||||
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||
expect(templatedQuery[0].azureLogAnalytics).toMatchObject({
|
||||
resources: ['resource1', 'resource2'],
|
||||
@@ -413,7 +316,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
...azureTraces,
|
||||
};
|
||||
|
||||
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
|
||||
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
|
||||
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||
expect(templatedQuery[0].azureTraces).toMatchObject({
|
||||
query: templateVariables.get('query')?.templateVariable.current.value,
|
||||
@@ -441,7 +344,7 @@ describe('AzureLogAnalyticsDatasource', () => {
|
||||
...query.azureTraces,
|
||||
...azureTraces,
|
||||
};
|
||||
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
|
||||
const templatedQuery = ctx.datasource.interpolateVariablesInQueries([query], {});
|
||||
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||
expect(templatedQuery[0].azureTraces).toMatchObject({
|
||||
resources: ['resource1', 'resource2'],
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import { map } from 'lodash';
|
||||
|
||||
import { DataSourceInstanceSettings, DataSourceRef, ScopedVars } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
import { DataSourceWithBackend, getTemplateSrv } from '@grafana/runtime';
|
||||
import { TimeSrv, getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
|
||||
import { isGUIDish } from '../components/ResourcePicker/utils';
|
||||
import ResponseParser from '../azure_monitor/response_parser';
|
||||
import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
||||
import LogAnalyticsQuerystringBuilder from '../log_analytics/querystring_builder';
|
||||
import {
|
||||
AzureAPIResponse,
|
||||
AzureDataSourceJsonData,
|
||||
AzureLogsVariable,
|
||||
AzureMonitorQuery,
|
||||
AzureQueryType,
|
||||
DatasourceValidationResult,
|
||||
Subscription,
|
||||
Workspace,
|
||||
} from '../types';
|
||||
import { interpolateVariable, routeNames } from '../utils/common';
|
||||
|
||||
import ResponseParser, { transformMetadataToKustoSchema } from './response_parser';
|
||||
|
||||
interface AdhocQuery {
|
||||
datasource: DataSourceRef;
|
||||
path: string;
|
||||
resultFormat: string;
|
||||
}
|
||||
import { transformMetadataToKustoSchema } from './utils';
|
||||
|
||||
export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
AzureMonitorQuery,
|
||||
@@ -70,7 +66,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
|
||||
const path = `${this.azureMonitorPath}?api-version=2019-03-01`;
|
||||
return await this.getResource(path).then((result: any) => {
|
||||
return await this.getResource<AzureAPIResponse<Subscription>>(path).then((result) => {
|
||||
return ResponseParser.parseSubscriptions(result);
|
||||
});
|
||||
}
|
||||
@@ -79,7 +75,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
const response = await this.getWorkspaceList(subscription);
|
||||
|
||||
return (
|
||||
map(response.value, (val: any) => {
|
||||
map(response.value, (val: Workspace) => {
|
||||
return {
|
||||
text: val.name,
|
||||
value: val.id,
|
||||
@@ -88,13 +84,13 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
);
|
||||
}
|
||||
|
||||
private getWorkspaceList(subscription: string): Promise<any> {
|
||||
private getWorkspaceList(subscription: string): Promise<AzureAPIResponse<Workspace>> {
|
||||
const subscriptionId = getTemplateSrv().replace(subscription || this.defaultSubscriptionId);
|
||||
|
||||
const workspaceListUrl =
|
||||
this.azureMonitorPath +
|
||||
`/${subscriptionId}/providers/Microsoft.OperationalInsights/workspaces?api-version=2017-04-26-preview`;
|
||||
return this.getResource(workspaceListUrl);
|
||||
return this.getResource<AzureAPIResponse<Workspace>>(workspaceListUrl);
|
||||
}
|
||||
|
||||
async getMetadata(resourceUri: string) {
|
||||
@@ -201,29 +197,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
return this.instanceSettings.jsonData.logAnalyticsDefaultWorkspace;
|
||||
}
|
||||
|
||||
private buildQuery(query: string, options: any, workspace: string): AdhocQuery[] {
|
||||
const querystringBuilder = new LogAnalyticsQuerystringBuilder(
|
||||
getTemplateSrv().replace(query, {}, interpolateVariable),
|
||||
options,
|
||||
'TimeGenerated'
|
||||
);
|
||||
|
||||
const querystring = querystringBuilder.generate().uriString;
|
||||
const path = isGUIDish(workspace)
|
||||
? `${this.resourcePath}/v1/workspaces/${workspace}/query?${querystring}`
|
||||
: `${this.resourcePath}/v1${workspace}/query?${querystring}`;
|
||||
|
||||
const queries = [
|
||||
{
|
||||
datasource: this.getRef(),
|
||||
path: path,
|
||||
resultFormat: 'table',
|
||||
},
|
||||
];
|
||||
|
||||
return queries;
|
||||
}
|
||||
|
||||
async getDefaultOrFirstSubscription(): Promise<string | undefined> {
|
||||
if (this.defaultSubscriptionId) {
|
||||
return this.defaultSubscriptionId;
|
||||
@@ -252,40 +225,6 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
return workspace;
|
||||
}
|
||||
|
||||
annotationQuery(options: any) {
|
||||
if (!options.annotation.rawQuery) {
|
||||
return Promise.reject({
|
||||
message: 'Query missing in annotation definition',
|
||||
});
|
||||
}
|
||||
|
||||
const queries = this.buildQuery(options.annotation.rawQuery, options, options.annotation.workspace);
|
||||
const promises = this.doQueries(queries);
|
||||
|
||||
return Promise.all(promises).then((results) => {
|
||||
const annotations = new ResponseParser(results).transformToAnnotations(options);
|
||||
return annotations;
|
||||
});
|
||||
}
|
||||
|
||||
doQueries(queries: AdhocQuery[]) {
|
||||
return map(queries, (query) => {
|
||||
return this.getResource(query.path)
|
||||
.then((result: any) => {
|
||||
return {
|
||||
result: result,
|
||||
query: query,
|
||||
};
|
||||
})
|
||||
.catch((err: any) => {
|
||||
throw {
|
||||
error: err,
|
||||
query: query,
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private validateDatasource(): DatasourceValidationResult | undefined {
|
||||
const authType = getAuthType(this.instanceSettings);
|
||||
|
||||
|
||||
@@ -1,303 +0,0 @@
|
||||
import { concat, find, flattenDeep, forEach, get, map } from 'lodash';
|
||||
|
||||
import { AnnotationEvent, dateTime, TimeSeries, VariableModel } from '@grafana/data';
|
||||
|
||||
import { AzureLogsTableData, AzureLogsVariable } from '../types';
|
||||
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
|
||||
|
||||
export default class ResponseParser {
|
||||
declare columns: string[];
|
||||
constructor(private results: any) {}
|
||||
|
||||
parseQueryResult(): any {
|
||||
let data: any[] = [];
|
||||
let columns: any[] = [];
|
||||
for (let i = 0; i < this.results.length; i++) {
|
||||
if (this.results[i].result.tables.length === 0) {
|
||||
continue;
|
||||
}
|
||||
columns = this.results[i].result.tables[0].columns;
|
||||
const rows = this.results[i].result.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: { refId: string; query: any }, columns: any[], rows: any): TimeSeries[] {
|
||||
const data: TimeSeries[] = [];
|
||||
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.meta = {
|
||||
executedQueryString: query.query,
|
||||
};
|
||||
});
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
parseTableResult(query: { refId: string; query: string }, columns: any[], rows: any[]): AzureLogsTableData {
|
||||
const tableResult: AzureLogsTableData = {
|
||||
type: 'table',
|
||||
columns: map(columns, (col) => {
|
||||
return { text: col.name, type: col.type };
|
||||
}),
|
||||
rows: rows,
|
||||
refId: query.refId,
|
||||
meta: {
|
||||
executedQueryString: query.query,
|
||||
},
|
||||
};
|
||||
|
||||
return tableResult;
|
||||
}
|
||||
|
||||
parseToVariables(): AzureLogsVariable[] {
|
||||
const queryResult = this.parseQueryResult();
|
||||
|
||||
const variables: AzureLogsVariable[] = [];
|
||||
forEach(queryResult, (result) => {
|
||||
forEach(flattenDeep(result.rows), (row) => {
|
||||
variables.push({
|
||||
text: row,
|
||||
value: row,
|
||||
} as AzureLogsVariable);
|
||||
});
|
||||
});
|
||||
|
||||
return variables;
|
||||
}
|
||||
|
||||
transformToAnnotations(options: any) {
|
||||
const queryResult = this.parseQueryResult();
|
||||
|
||||
const list: AnnotationEvent[] = [];
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
static findOrCreateBucket(data: TimeSeries[], target: any): TimeSeries {
|
||||
let dataTarget: any = find(data, ['target', target]);
|
||||
if (!dataTarget) {
|
||||
dataTarget = { target: target, datapoints: [], refId: '', query: '' };
|
||||
data.push(dataTarget);
|
||||
}
|
||||
|
||||
return dataTarget;
|
||||
}
|
||||
|
||||
static dateTimeToEpoch(dateTimeValue: any) {
|
||||
return dateTime(dateTimeValue).valueOf();
|
||||
}
|
||||
|
||||
static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
|
||||
const list: Array<{ text: string; value: string }> = [];
|
||||
|
||||
if (!result) {
|
||||
return list;
|
||||
}
|
||||
|
||||
const valueFieldName = 'subscriptionId';
|
||||
const textFieldName = 'displayName';
|
||||
for (let i = 0; i < result.value.length; i++) {
|
||||
if (!find(list, ['value', get(result.value[i], valueFieldName)])) {
|
||||
list.push({
|
||||
text: `${get(result.value[i], textFieldName)}`,
|
||||
value: get(result.value[i], valueFieldName),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
||||
// matches (name):(type) = (defaultValue)
|
||||
// e.g. fromRangeStart:datetime = datetime(null)
|
||||
// - name: fromRangeStart
|
||||
// - type: datetime
|
||||
// - defaultValue: datetime(null)
|
||||
const METADATA_FUNCTION_PARAMS = /([\w\W]+):([\w]+)(?:\s?=\s?([\w\W]+))?/;
|
||||
|
||||
function transformMetadataFunction(sourceSchema: AzureLogAnalyticsMetadata) {
|
||||
if (!sourceSchema.functions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sourceSchema.functions.map((fn) => {
|
||||
const params =
|
||||
fn.parameters &&
|
||||
fn.parameters
|
||||
.split(', ')
|
||||
.map((arg) => {
|
||||
const match = arg.match(METADATA_FUNCTION_PARAMS);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, name, type, defaultValue] = match;
|
||||
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
defaultValue,
|
||||
cslDefaultValue: defaultValue,
|
||||
};
|
||||
})
|
||||
.filter(<T>(v: T): v is Exclude<T, undefined> => !!v);
|
||||
|
||||
return {
|
||||
name: fn.name,
|
||||
body: fn.body,
|
||||
inputParameters: params || [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function transformMetadataToKustoSchema(
|
||||
sourceSchema: AzureLogAnalyticsMetadata,
|
||||
nameOrIdOrSomething: string,
|
||||
templateVariables: VariableModel[]
|
||||
) {
|
||||
const database = {
|
||||
name: nameOrIdOrSomething,
|
||||
tables: sourceSchema.tables,
|
||||
functions: transformMetadataFunction(sourceSchema),
|
||||
majorVersion: 0,
|
||||
minorVersion: 0,
|
||||
};
|
||||
|
||||
// Adding macros as known functions
|
||||
database.functions.push(
|
||||
{
|
||||
name: '$__timeFilter',
|
||||
body: '{ true }',
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'timeColumn',
|
||||
type: 'System.String',
|
||||
defaultValue: '""',
|
||||
cslDefaultValue: '""',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '$__timeFrom',
|
||||
body: '{ datetime(2018-06-05T18:09:58.907Z) }',
|
||||
inputParameters: [],
|
||||
},
|
||||
{
|
||||
name: '$__timeTo',
|
||||
body: '{ datetime(2018-06-05T20:09:58.907Z) }',
|
||||
inputParameters: [],
|
||||
},
|
||||
{
|
||||
name: '$__escapeMulti',
|
||||
body: `{ @'\\grafana-vm\Network(eth0)\Total', @'\\hello!'}`,
|
||||
inputParameters: [
|
||||
{
|
||||
name: '$myVar',
|
||||
type: 'System.String',
|
||||
defaultValue: '$myVar',
|
||||
cslDefaultValue: '$myVar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '$__contains',
|
||||
body: `{ colName in ('value1','value2') }`,
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'colName',
|
||||
type: 'System.String',
|
||||
defaultValue: 'colName',
|
||||
cslDefaultValue: 'colName',
|
||||
},
|
||||
{
|
||||
name: '$myVar',
|
||||
type: 'System.String',
|
||||
defaultValue: '$myVar',
|
||||
cslDefaultValue: '$myVar',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
// Adding macros as global parameters
|
||||
const globalParameters = templateVariables.map((v) => {
|
||||
return {
|
||||
name: `$${v.name}`,
|
||||
type: 'dynamic',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
clusterType: 'Engine',
|
||||
cluster: {
|
||||
connectionString: nameOrIdOrSomething,
|
||||
databases: [database],
|
||||
},
|
||||
database: database,
|
||||
globalParameters,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { VariableModel } from '@grafana/data';
|
||||
|
||||
import { AzureLogAnalyticsMetadata } from '../types/logAnalyticsMetadata';
|
||||
|
||||
// matches (name):(type) = (defaultValue)
|
||||
// e.g. fromRangeStart:datetime = datetime(null)
|
||||
// - name: fromRangeStart
|
||||
// - type: datetime
|
||||
// - defaultValue: datetime(null)
|
||||
const METADATA_FUNCTION_PARAMS = /([\w\W]+):([\w]+)(?:\s?=\s?([\w\W]+))?/;
|
||||
|
||||
function transformMetadataFunction(sourceSchema: AzureLogAnalyticsMetadata) {
|
||||
if (!sourceSchema.functions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return sourceSchema.functions.map((fn) => {
|
||||
const params =
|
||||
fn.parameters &&
|
||||
fn.parameters
|
||||
.split(', ')
|
||||
.map((arg) => {
|
||||
const match = arg.match(METADATA_FUNCTION_PARAMS);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [, name, type, defaultValue] = match;
|
||||
|
||||
return {
|
||||
name,
|
||||
type,
|
||||
defaultValue,
|
||||
cslDefaultValue: defaultValue,
|
||||
};
|
||||
})
|
||||
.filter(<T>(v: T): v is Exclude<T, undefined> => !!v);
|
||||
|
||||
return {
|
||||
name: fn.name,
|
||||
body: fn.body,
|
||||
inputParameters: params || [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function transformMetadataToKustoSchema(
|
||||
sourceSchema: AzureLogAnalyticsMetadata,
|
||||
nameOrIdOrSomething: string,
|
||||
templateVariables: VariableModel[]
|
||||
) {
|
||||
const database = {
|
||||
name: nameOrIdOrSomething,
|
||||
tables: sourceSchema.tables,
|
||||
functions: transformMetadataFunction(sourceSchema),
|
||||
majorVersion: 0,
|
||||
minorVersion: 0,
|
||||
};
|
||||
|
||||
// Adding macros as known functions
|
||||
database.functions.push(
|
||||
{
|
||||
name: '$__timeFilter',
|
||||
body: '{ true }',
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'timeColumn',
|
||||
type: 'System.String',
|
||||
defaultValue: '""',
|
||||
cslDefaultValue: '""',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '$__timeFrom',
|
||||
body: '{ datetime(2018-06-05T18:09:58.907Z) }',
|
||||
inputParameters: [],
|
||||
},
|
||||
{
|
||||
name: '$__timeTo',
|
||||
body: '{ datetime(2018-06-05T20:09:58.907Z) }',
|
||||
inputParameters: [],
|
||||
},
|
||||
{
|
||||
name: '$__escapeMulti',
|
||||
body: `{ @'\\grafana-vm\Network(eth0)\Total', @'\\hello!'}`,
|
||||
inputParameters: [
|
||||
{
|
||||
name: '$myVar',
|
||||
type: 'System.String',
|
||||
defaultValue: '$myVar',
|
||||
cslDefaultValue: '$myVar',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: '$__contains',
|
||||
body: `{ colName in ('value1','value2') }`,
|
||||
inputParameters: [
|
||||
{
|
||||
name: 'colName',
|
||||
type: 'System.String',
|
||||
defaultValue: 'colName',
|
||||
cslDefaultValue: 'colName',
|
||||
},
|
||||
{
|
||||
name: '$myVar',
|
||||
type: 'System.String',
|
||||
defaultValue: '$myVar',
|
||||
cslDefaultValue: '$myVar',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
// Adding macros as global parameters
|
||||
const globalParameters = templateVariables.map((v) => {
|
||||
return {
|
||||
name: `$${v.name}`,
|
||||
type: 'dynamic',
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
clusterType: 'Engine',
|
||||
cluster: {
|
||||
connectionString: nameOrIdOrSomething,
|
||||
databases: [database],
|
||||
},
|
||||
database: database,
|
||||
globalParameters,
|
||||
};
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import createMockQuery from '../__mocks__/query';
|
||||
import { createTemplateVariables } from '../__mocks__/utils';
|
||||
import { multiVariable, singleVariable, subscriptionsVariable } from '../__mocks__/variables';
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import { AzureDataSourceJsonData, AzureMonitorLocationsResponse, AzureQueryType } from '../types';
|
||||
import { AzureAPIResponse, AzureDataSourceJsonData, AzureQueryType, Location } from '../types';
|
||||
|
||||
const templateSrv = new TemplateSrv();
|
||||
|
||||
@@ -477,7 +477,7 @@ describe('AzureMonitorDatasource', () => {
|
||||
});
|
||||
|
||||
describe('When performing getLocations', () => {
|
||||
const sub1Response: AzureMonitorLocationsResponse = {
|
||||
const sub1Response: AzureAPIResponse<Location> = {
|
||||
value: [
|
||||
{
|
||||
id: '/subscriptions/mock-subscription-id-1/locations/northeurope',
|
||||
@@ -497,7 +497,7 @@ describe('AzureMonitorDatasource', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const sub2Response: AzureMonitorLocationsResponse = {
|
||||
const sub2Response: AzureAPIResponse<Location> = {
|
||||
value: [
|
||||
{
|
||||
id: '/subscriptions/mock-subscription-id-2/locations/eastus2',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Namespace } from 'i18next';
|
||||
import { find, startsWith } from 'lodash';
|
||||
|
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
@@ -8,11 +9,8 @@ import { getAuthType, getAzureCloud, getAzurePortalUrl } from '../credentials';
|
||||
import TimegrainConverter from '../time_grain_converter';
|
||||
import {
|
||||
AzureDataSourceJsonData,
|
||||
AzureMonitorMetricNamespacesResponse,
|
||||
AzureMonitorMetricNamesResponse,
|
||||
AzureMonitorMetricsMetadataResponse,
|
||||
AzureMonitorQuery,
|
||||
AzureMonitorResourceGroupsResponse,
|
||||
AzureQueryType,
|
||||
DatasourceValidationResult,
|
||||
GetMetricNamespacesQuery,
|
||||
@@ -21,8 +19,12 @@ import {
|
||||
AzureMetricQuery,
|
||||
AzureMonitorLocations,
|
||||
AzureMonitorProvidersResponse,
|
||||
AzureMonitorLocationsResponse,
|
||||
AzureAPIResponse,
|
||||
AzureGetResourceNamesQuery,
|
||||
Subscription,
|
||||
Location,
|
||||
ResourceGroup,
|
||||
Metric,
|
||||
} from '../types';
|
||||
import { routeNames } from '../utils/common';
|
||||
import migrateQuery from '../utils/migrateQuery';
|
||||
@@ -162,7 +164,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.getResource(`${this.resourcePath}/subscriptions?api-version=2019-03-01`).then((result: any) => {
|
||||
return this.getResource<AzureAPIResponse<Subscription>>(
|
||||
`${this.resourcePath}/subscriptions?api-version=2019-03-01`
|
||||
).then((result) => {
|
||||
return ResponseParser.parseSubscriptions(result);
|
||||
});
|
||||
}
|
||||
@@ -170,8 +174,8 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
getResourceGroups(subscriptionId: string) {
|
||||
return this.getResource(
|
||||
`${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
|
||||
).then((result: AzureMonitorResourceGroupsResponse) => {
|
||||
return ResponseParser.parseResponseValues(result, 'name', 'name');
|
||||
).then((result: AzureAPIResponse<ResourceGroup>) => {
|
||||
return ResponseParser.parseResponseValues<ResourceGroup>(result, 'name', 'name');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -199,7 +203,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
if (skipToken) {
|
||||
url += `&$skiptoken=${skipToken}`;
|
||||
}
|
||||
return this.getResource(url).then(async (result: any) => {
|
||||
return this.getResource(url).then(async (result) => {
|
||||
let list: Array<{ text: string; value: string }> = [];
|
||||
if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) {
|
||||
list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts');
|
||||
@@ -239,7 +243,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
this.templateSrv
|
||||
);
|
||||
return this.getResource(url)
|
||||
.then((result: AzureMonitorMetricNamespacesResponse) => {
|
||||
.then((result: AzureAPIResponse<Namespace>) => {
|
||||
return ResponseParser.parseResponseValues(
|
||||
result,
|
||||
'properties.metricNamespaceName',
|
||||
@@ -273,7 +277,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
this.replaceSingleTemplateVariables(query),
|
||||
this.templateSrv
|
||||
);
|
||||
return this.getResource(url).then((result: AzureMonitorMetricNamesResponse) => {
|
||||
return this.getResource(url).then((result: AzureAPIResponse<Metric>) => {
|
||||
return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
|
||||
});
|
||||
}
|
||||
@@ -366,7 +370,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
const locationMap = new Map<string, AzureMonitorLocations>();
|
||||
for (const subscription of subscriptions) {
|
||||
const subLocations = ResponseParser.parseLocations(
|
||||
await this.getResource<AzureMonitorLocationsResponse>(
|
||||
await this.getResource<AzureAPIResponse<Location>>(
|
||||
`${routeNames.azureMonitor}/subscriptions/${this.templateSrv.replace(subscription)}/locations?api-version=${
|
||||
this.locationsApiVersion
|
||||
}`
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { find, get } from 'lodash';
|
||||
|
||||
import { FetchResponse } from '@grafana/runtime';
|
||||
|
||||
import TimeGrainConverter from '../time_grain_converter';
|
||||
import {
|
||||
AzureMonitorLocalizedValue,
|
||||
@@ -7,11 +9,14 @@ import {
|
||||
AzureMonitorMetricAvailabilityMetadata,
|
||||
AzureMonitorMetricsMetadataResponse,
|
||||
AzureMonitorOption,
|
||||
AzureMonitorLocationsResponse,
|
||||
AzureAPIResponse,
|
||||
Location,
|
||||
Subscription,
|
||||
Resource,
|
||||
} from '../types';
|
||||
export default class ResponseParser {
|
||||
static parseResponseValues(
|
||||
result: any,
|
||||
static parseResponseValues<T>(
|
||||
result: AzureAPIResponse<T>,
|
||||
textFieldName: string,
|
||||
valueFieldName: string
|
||||
): Array<{ text: string; value: string }> {
|
||||
@@ -35,7 +40,10 @@ export default class ResponseParser {
|
||||
return list;
|
||||
}
|
||||
|
||||
static parseResourceNames(result: any, metricNamespace?: string): Array<{ text: string; value: string }> {
|
||||
static parseResourceNames(
|
||||
result: AzureAPIResponse<Resource>,
|
||||
metricNamespace?: string
|
||||
): Array<{ text: string; value: string }> {
|
||||
const list: Array<{ text: string; value: string }> = [];
|
||||
|
||||
if (!result) {
|
||||
@@ -110,7 +118,7 @@ export default class ResponseParser {
|
||||
});
|
||||
}
|
||||
|
||||
static parseSubscriptions(result: any): Array<{ text: string; value: string }> {
|
||||
static parseSubscriptions(result: AzureAPIResponse<Subscription>): Array<{ text: string; value: string }> {
|
||||
const list: Array<{ text: string; value: string }> = [];
|
||||
|
||||
if (!result) {
|
||||
@@ -131,7 +139,9 @@ export default class ResponseParser {
|
||||
return list;
|
||||
}
|
||||
|
||||
static parseSubscriptionsForSelect(result: any): Array<{ label: string; value: string }> {
|
||||
static parseSubscriptionsForSelect(
|
||||
result?: FetchResponse<AzureAPIResponse<Subscription>>
|
||||
): Array<{ label: string; value: string }> {
|
||||
const list: Array<{ label: string; value: string }> = [];
|
||||
|
||||
if (!result) {
|
||||
@@ -152,28 +162,7 @@ export default class ResponseParser {
|
||||
return list;
|
||||
}
|
||||
|
||||
static parseWorkspacesForSelect(result: any): Array<{ label: string; value: string }> {
|
||||
const list: Array<{ label: string; value: string }> = [];
|
||||
|
||||
if (!result) {
|
||||
return list;
|
||||
}
|
||||
|
||||
const valueFieldName = 'customerId';
|
||||
const textFieldName = 'name';
|
||||
for (let i = 0; i < result.data.value.length; i++) {
|
||||
if (!find(list, ['value', get(result.data.value[i].properties, valueFieldName)])) {
|
||||
list.push({
|
||||
label: get(result.data.value[i], textFieldName),
|
||||
value: get(result.data.value[i].properties, valueFieldName),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
static parseLocations(result: AzureMonitorLocationsResponse) {
|
||||
static parseLocations(result: AzureAPIResponse<Location>) {
|
||||
const locations: AzureMonitorLocations[] = [];
|
||||
|
||||
if (!result) {
|
||||
|
||||
@@ -3,14 +3,13 @@ import { set, get } from 'lodash';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||
|
||||
import { Context, createContext } from '../__mocks__/datasource';
|
||||
import createMockQuery from '../__mocks__/query';
|
||||
import { createTemplateVariables } from '../__mocks__/utils';
|
||||
import { multiVariable, singleVariable, subscriptionsVariable } from '../__mocks__/variables';
|
||||
import AzureMonitorDatasource from '../datasource';
|
||||
import { AzureQueryType } from '../types';
|
||||
|
||||
import AzureResourceGraphDatasource from './azure_resource_graph_datasource';
|
||||
|
||||
const templateSrv = new TemplateSrv({
|
||||
getVariables: () => [subscriptionsVariable, singleVariable, multiVariable],
|
||||
getVariableWithName: jest.fn(),
|
||||
@@ -32,15 +31,15 @@ describe('AzureResourceGraphDatasource', () => {
|
||||
datasourceRequestMock.mockImplementation(jest.fn());
|
||||
});
|
||||
|
||||
const ctx: any = {};
|
||||
let ctx: Context;
|
||||
|
||||
beforeEach(() => {
|
||||
ctx.instanceSettings = {
|
||||
url: 'http://azureresourcegraphapi',
|
||||
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f', cloudName: 'azuremonitor' },
|
||||
};
|
||||
|
||||
ctx.ds = new AzureResourceGraphDatasource(ctx.instanceSettings);
|
||||
ctx = createContext({
|
||||
instanceSettings: {
|
||||
url: 'http://azureresourcegraphapi',
|
||||
jsonData: { subscriptionId: '9935389e-9122-4ef9-95f9-1513dd24753f', cloudName: 'azuremonitor' },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing interpolateVariablesInQueries for azure_resource_graph', () => {
|
||||
@@ -51,7 +50,7 @@ describe('AzureResourceGraphDatasource', () => {
|
||||
it('should return a query unchanged if no template variables are provided', () => {
|
||||
const query = createMockQuery();
|
||||
query.queryType = AzureQueryType.AzureResourceGraph;
|
||||
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
|
||||
const templatedQuery = ctx.datasource.azureResourceGraphDatasource.interpolateVariablesInQueries([query], {});
|
||||
expect(templatedQuery[0]).toEqual(query);
|
||||
});
|
||||
|
||||
@@ -70,7 +69,7 @@ describe('AzureResourceGraphDatasource', () => {
|
||||
...query.azureResourceGraph,
|
||||
...azureResourceGraph,
|
||||
};
|
||||
const templatedQuery = ctx.ds.interpolateVariablesInQueries([query], {});
|
||||
const templatedQuery = ctx.datasource.azureResourceGraphDatasource.interpolateVariablesInQueries([query], {});
|
||||
expect(templatedQuery[0]).toHaveProperty('datasource');
|
||||
for (const [path, templateVariable] of templateVariables.entries()) {
|
||||
expect(get(templatedQuery[0].azureResourceGraph, path)).toEqual(
|
||||
@@ -86,53 +85,63 @@ describe('AzureResourceGraphDatasource', () => {
|
||||
});
|
||||
|
||||
it('should expand single value template variable', () => {
|
||||
const target = {
|
||||
const target = createMockQuery({
|
||||
subscriptions: [],
|
||||
azureResourceGraph: {
|
||||
query: 'Resources | $var1',
|
||||
resultFormat: '',
|
||||
},
|
||||
};
|
||||
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({
|
||||
azureResourceGraph: { query: 'Resources | var1-foo', resultFormat: 'table' },
|
||||
queryType: 'Azure Resource Graph',
|
||||
subscriptions: [],
|
||||
});
|
||||
expect(ctx.datasource.azureResourceGraphDatasource.applyTemplateVariables(target, {})).toEqual(
|
||||
expect.objectContaining({
|
||||
...target,
|
||||
azureResourceGraph: { query: 'Resources | var1-foo', resultFormat: 'table' },
|
||||
queryType: 'Azure Resource Graph',
|
||||
subscriptions: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should expand multi value template variable', () => {
|
||||
const target = {
|
||||
const target = createMockQuery({
|
||||
subscriptions: [],
|
||||
azureResourceGraph: {
|
||||
query: 'resources | where $__contains(name, $var3)',
|
||||
resultFormat: '',
|
||||
},
|
||||
};
|
||||
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({
|
||||
azureResourceGraph: {
|
||||
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
|
||||
resultFormat: 'table',
|
||||
},
|
||||
queryType: 'Azure Resource Graph',
|
||||
subscriptions: [],
|
||||
});
|
||||
expect(ctx.datasource.azureResourceGraphDatasource.applyTemplateVariables(target, {})).toEqual(
|
||||
expect.objectContaining({
|
||||
...target,
|
||||
azureResourceGraph: {
|
||||
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
|
||||
resultFormat: 'table',
|
||||
},
|
||||
queryType: 'Azure Resource Graph',
|
||||
subscriptions: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply subscription variable', () => {
|
||||
const target = {
|
||||
const target = createMockQuery({
|
||||
subscriptions: ['$subs'],
|
||||
azureResourceGraph: {
|
||||
query: 'resources | where $__contains(name, $var3)',
|
||||
resultFormat: '',
|
||||
},
|
||||
};
|
||||
expect(ctx.ds.applyTemplateVariables(target)).toStrictEqual({
|
||||
azureResourceGraph: {
|
||||
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
|
||||
resultFormat: 'table',
|
||||
},
|
||||
queryType: 'Azure Resource Graph',
|
||||
subscriptions: ['sub-foo', 'sub-baz'],
|
||||
});
|
||||
expect(ctx.datasource.azureResourceGraphDatasource.applyTemplateVariables(target, {})).toEqual(
|
||||
expect.objectContaining({
|
||||
azureResourceGraph: {
|
||||
query: `resources | where $__contains(name, 'var3-foo','var3-baz')`,
|
||||
resultFormat: 'table',
|
||||
},
|
||||
queryType: 'Azure Resource Graph',
|
||||
subscriptions: ['sub-foo', 'sub-baz'],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('When performing targetContainsTemplate', () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
|
||||
const variableNames = templateSrv.getVariables().map((v) => `$${v.name}`);
|
||||
const subscriptionVar = _.find(target.subscriptions, (sub) => _.includes(variableNames, sub));
|
||||
const interpolatedSubscriptions = templateSrv
|
||||
.replace(subscriptionVar, scopedVars, (v: any) => v)
|
||||
.replace(subscriptionVar, scopedVars, (v: string[] | string) => v)
|
||||
.split(',')
|
||||
.filter((v) => v.length > 0);
|
||||
const subscriptions = [
|
||||
|
||||
@@ -6,7 +6,13 @@ import { Alert, SecureSocksProxySettings } from '@grafana/ui';
|
||||
import { config } from 'app/core/config';
|
||||
|
||||
import ResponseParser from '../azure_monitor/response_parser';
|
||||
import { AzureDataSourceJsonData, AzureDataSourceSecureJsonData, AzureDataSourceSettings } from '../types';
|
||||
import {
|
||||
AzureAPIResponse,
|
||||
AzureDataSourceJsonData,
|
||||
AzureDataSourceSecureJsonData,
|
||||
AzureDataSourceSettings,
|
||||
Subscription,
|
||||
} from '../types';
|
||||
import { routeNames } from '../utils/common';
|
||||
|
||||
import { MonitorConfig } from './MonitorConfig';
|
||||
@@ -62,7 +68,7 @@ export class ConfigEditor extends PureComponent<Props, State> {
|
||||
const query = `?api-version=2019-03-01`;
|
||||
try {
|
||||
const result = await getBackendSrv()
|
||||
.fetch({
|
||||
.fetch<AzureAPIResponse<Subscription>>({
|
||||
url: this.baseURL + query,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Uri } from 'monaco-editor';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
import { CodeEditor, Monaco, MonacoEditor } from '@grafana/ui';
|
||||
@@ -15,7 +16,7 @@ interface MonacoPromise {
|
||||
interface MonacoLanguages {
|
||||
kusto: {
|
||||
getKustoWorker: () => Promise<
|
||||
(url: any) => Promise<{
|
||||
(url: Uri) => Promise<{
|
||||
setSchema: (schema: any, clusterUrl: string, name: string) => void;
|
||||
}>
|
||||
>;
|
||||
|
||||
@@ -139,10 +139,6 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
|
||||
return !!subQuery && this.templateSrv.containsTemplate(subQuery);
|
||||
}
|
||||
|
||||
async annotationQuery(options: any) {
|
||||
return this.azureLogAnalyticsDatasource.annotationQuery(options);
|
||||
}
|
||||
|
||||
/* Azure Monitor REST API methods */
|
||||
getResourceGroups(subscriptionId: string) {
|
||||
return this.azureMonitorDatasource.getResourceGroups(this.templateSrv.replace(subscriptionId));
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
import { dateTime } from '@grafana/data';
|
||||
|
||||
import LogAnalyticsQuerystringBuilder from './querystring_builder';
|
||||
|
||||
describe('LogAnalyticsDatasource', () => {
|
||||
let builder: LogAnalyticsQuerystringBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = new LogAnalyticsQuerystringBuilder(
|
||||
'query=Tablename | where $__timeFilter()',
|
||||
{
|
||||
interval: '5m',
|
||||
range: {
|
||||
from: dateTime().subtract(24, 'hours'),
|
||||
to: dateTime(),
|
||||
},
|
||||
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 $__timeFrom and $__timeTo is in the query and range is until now', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = 'query=Tablename | where myTime >= $__timeFrom() and myTime <= $__timeTo()';
|
||||
});
|
||||
|
||||
it('should replace $__timeFrom and $__timeTo 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%20datetime(');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when using $__timeFrom and $__timeTo is in the query and range is a specific interval', () => {
|
||||
beforeEach(() => {
|
||||
builder.rawQueryString = 'query=Tablename | where myTime >= $__timeFrom() and myTime <= $__timeTo()';
|
||||
builder.options.range.to = dateTime().subtract(1, 'hour');
|
||||
builder.options.rangeRaw.to = 'now-1h';
|
||||
});
|
||||
|
||||
it('should replace $__timeFrom and $__timeTo 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)`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there is no raw range', () => {
|
||||
it('should still generate a time filter condition', () => {
|
||||
builder = new LogAnalyticsQuerystringBuilder(
|
||||
'query=Tablename | where $__timeFilter()',
|
||||
{
|
||||
interval: '5m',
|
||||
range: {
|
||||
from: dateTime().subtract(24, 'hours'),
|
||||
to: dateTime(),
|
||||
},
|
||||
},
|
||||
'TimeGenerated'
|
||||
);
|
||||
const query = builder.generate().uriString;
|
||||
|
||||
expect(query).toContain('where%20TimeGenerated%20%20%3E%3D%20datetime(');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,85 +0,0 @@
|
||||
import { dateTime } from '@grafana/data';
|
||||
|
||||
export default class LogAnalyticsQuerystringBuilder {
|
||||
constructor(public rawQueryString: string, public options: any, public defaultTimeField: any) {}
|
||||
|
||||
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);
|
||||
}
|
||||
if (p1 === 'timeFrom') {
|
||||
return this.getFrom(this.options);
|
||||
}
|
||||
if (p1 === 'timeTo') {
|
||||
return this.getUntil(this.options);
|
||||
}
|
||||
|
||||
return match;
|
||||
});
|
||||
queryString = queryString.replace(/\$__interval/gi, this.options.interval);
|
||||
}
|
||||
const rawQuery = queryString;
|
||||
queryString = encodeURIComponent(queryString);
|
||||
const uriString = `query=${queryString}`;
|
||||
|
||||
return { uriString, rawQuery };
|
||||
}
|
||||
|
||||
getFrom(options: any) {
|
||||
const from = options.range.from;
|
||||
return `datetime(${dateTime(from).startOf('minute').toISOString()})`;
|
||||
}
|
||||
|
||||
getUntil(options: any) {
|
||||
if (options.rangeRaw?.to === 'now') {
|
||||
const now = Date.now();
|
||||
return `datetime(${dateTime(now).startOf('minute').toISOString()})`;
|
||||
} else {
|
||||
const until = options.range.to;
|
||||
return `datetime(${dateTime(until).startOf('minute').toISOString()})`;
|
||||
}
|
||||
}
|
||||
|
||||
getTimeFilter(timeFieldArg: any, options: any) {
|
||||
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(', ');
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { includes, filter } from 'lodash';
|
||||
import { rangeUtil } from '@grafana/data';
|
||||
|
||||
export default class TimeGrainConverter {
|
||||
static createISO8601Duration(timeGrain: string | number, timeGrainUnit: any) {
|
||||
static createISO8601Duration(timeGrain: string | number, timeGrainUnit: string) {
|
||||
const timeIntervals = ['hour', 'minute', 'h', 'm'];
|
||||
if (includes(timeIntervals, timeGrainUnit)) {
|
||||
return `PT${timeGrain}${timeGrainUnit[0].toUpperCase()}`;
|
||||
@@ -33,7 +33,7 @@ export default class TimeGrainConverter {
|
||||
return TimeGrainConverter.createISO8601Duration(timeGrain, unit);
|
||||
}
|
||||
|
||||
static findClosestTimeGrain(interval: any, allowedTimeGrains: string[]) {
|
||||
static findClosestTimeGrain(interval: string, allowedTimeGrains: string[]) {
|
||||
const timeGrains = filter(allowedTimeGrains, (o) => o !== 'auto');
|
||||
|
||||
let closest = timeGrains[0];
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
DataSourceSettings,
|
||||
PanelData,
|
||||
SelectableValue,
|
||||
TableData,
|
||||
} from '@grafana/data';
|
||||
|
||||
import Datasource from '../datasource';
|
||||
@@ -101,23 +100,6 @@ export interface AzureMonitorMetricMetadataItem {
|
||||
metricAvailabilities?: AzureMonitorMetricAvailabilityMetadata[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNamespacesResponse {
|
||||
value: AzureMonitorMetricNamespaceItem[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNamespaceItem {
|
||||
name: string;
|
||||
properties: { metricNamespacename: string };
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNamesResponse {
|
||||
value: AzureMonitorMetricNameItem[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNameItem {
|
||||
name: { value: string; localizedValue: string };
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricAvailabilityMetadata {
|
||||
timeGrain: string;
|
||||
retention: string;
|
||||
@@ -128,30 +110,11 @@ export interface AzureMonitorLocalizedValue {
|
||||
localizedValue: string;
|
||||
}
|
||||
|
||||
export interface AzureMonitorResourceGroupsResponse {
|
||||
data: {
|
||||
value: Array<{ name: string }>;
|
||||
};
|
||||
status: number;
|
||||
statusText: string;
|
||||
}
|
||||
|
||||
export interface AzureLogsVariable {
|
||||
text: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface AzureLogsTableData extends TableData {
|
||||
columns: AzureLogsTableColumn[];
|
||||
rows: any[];
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface AzureLogsTableColumn {
|
||||
text: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface AzureMonitorOption<T = string> {
|
||||
label: string;
|
||||
value: T;
|
||||
@@ -293,11 +256,17 @@ export interface ProviderResourceType {
|
||||
capabilities: string;
|
||||
}
|
||||
|
||||
export interface AzureMonitorLocationsResponse {
|
||||
value: Location[];
|
||||
export interface AzureAPIResponse<T> {
|
||||
value: T[];
|
||||
count?: {
|
||||
type: string;
|
||||
value: number;
|
||||
};
|
||||
status?: number;
|
||||
statusText?: string;
|
||||
}
|
||||
|
||||
interface Location {
|
||||
export interface Location {
|
||||
id: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
@@ -319,3 +288,100 @@ interface LocationPairedRegion {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Subscription {
|
||||
id: string;
|
||||
authorizationSource: string;
|
||||
subscriptionId: string;
|
||||
tenantId: string;
|
||||
displayName: string;
|
||||
state: string;
|
||||
subscriptionPolicies: {
|
||||
locationPlacementId: string;
|
||||
quotaId: string;
|
||||
spendingLimit: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
properties: {
|
||||
customerId: string;
|
||||
provisioningState: string;
|
||||
sku: {
|
||||
name: string;
|
||||
};
|
||||
retentionInDays: number;
|
||||
publicNetworkAccessForQuery: string;
|
||||
publicNetworkAccessForIngestion: string;
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
location: string;
|
||||
tags: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface Resource {
|
||||
changedTime: string;
|
||||
createdTime: string;
|
||||
extendedLocation: { name: string; type: string };
|
||||
id: string;
|
||||
identity: { principalId: string; tenantId: string; type: string; userAssignedIdentities: string[] };
|
||||
kind: string;
|
||||
location: string;
|
||||
managedBy: string;
|
||||
name: string;
|
||||
plan: { name: string; product: string; promotionCode: string; publisher: string; version: string };
|
||||
properties: Record<string, string>;
|
||||
provisioningState: string;
|
||||
sku: { capacity: number; family: string; model: string; name: string; size: string; tier: string };
|
||||
tags: Record<string, string>;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface ResourceGroup {
|
||||
id: string;
|
||||
location: string;
|
||||
managedBy: string;
|
||||
name: string;
|
||||
properties: { provisioningState: string };
|
||||
tags: object;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Namespace {
|
||||
classification: {
|
||||
Custom: string;
|
||||
Platform: string;
|
||||
Qos: string;
|
||||
};
|
||||
id: string;
|
||||
name: string;
|
||||
properties: { metricNamespaceName: string };
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface Metric {
|
||||
displayDescription: string;
|
||||
errorCode: string;
|
||||
errorMessage: string;
|
||||
id: string;
|
||||
name: AzureMonitorLocalizedValue;
|
||||
timeseries: Array<{ data: MetricValue[]; metadatavalues: MetricMetadataValue[] }>;
|
||||
type: string;
|
||||
unit: string;
|
||||
}
|
||||
|
||||
interface MetricValue {
|
||||
average: number;
|
||||
count: number;
|
||||
maximum: number;
|
||||
minimum: number;
|
||||
timeStamp: string;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface MetricMetadataValue {
|
||||
name: AzureMonitorLocalizedValue;
|
||||
value: string;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { map } from 'lodash';
|
||||
|
||||
import { rangeUtil, SelectableValue } from '@grafana/data';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { VariableWithMultiSupport } from 'app/features/variables/types';
|
||||
|
||||
import TimegrainConverter from '../time_grain_converter';
|
||||
import { AzureMonitorOption, VariableOptionGroup } from '../types';
|
||||
|
||||
export const hasOption = (options: AzureMonitorOption[], value: string): boolean =>
|
||||
@@ -37,16 +36,6 @@ export const addValueToOptions = (
|
||||
return options;
|
||||
};
|
||||
|
||||
export function convertTimeGrainsToMs<T extends { value: string }>(timeGrains: T[]) {
|
||||
const allowedTimeGrainsMs: number[] = [];
|
||||
timeGrains.forEach((tg: any) => {
|
||||
if (tg.value !== 'auto') {
|
||||
allowedTimeGrainsMs.push(rangeUtil.intervalToMs(TimegrainConverter.createKbnUnitFromISO8601Duration(tg.value)));
|
||||
}
|
||||
});
|
||||
return allowedTimeGrainsMs;
|
||||
}
|
||||
|
||||
// Route definitions shared with the backend.
|
||||
// Check: /pkg/tsdb/azuremonitor/azuremonitor-resource-handler.go <registerRoutes>
|
||||
export const routeNames = {
|
||||
@@ -56,7 +45,10 @@ export const routeNames = {
|
||||
resourceGraph: 'resourcegraph',
|
||||
};
|
||||
|
||||
export function interpolateVariable(value: any, variable: VariableWithMultiSupport) {
|
||||
export function interpolateVariable(
|
||||
value: string | number | Array<string | number>,
|
||||
variable: VariableWithMultiSupport
|
||||
) {
|
||||
if (typeof value === 'string') {
|
||||
// When enabling multiple responses, quote the value to mimic the array result below
|
||||
// even if only one response is selected. This does not apply if only the "include all"
|
||||
|
||||
Reference in New Issue
Block a user