AzureMonitor: Use Resource Picker in Metrics Query Editor (#47164)

* wip: new metrics query editor

* prepend subscriptions to url path

* remove duplicated setQueryValue file

* add tests for new metrics query editor

* wip start extracting resource field into a shared component

* fix query editor test

* fix up backend tests

* move azure monitor specific getters to azure_monitor_datasource

* use existing useAsyncState hook

* extract useAsyncState into separate file

* add datahooks tests

* extract resource field component

* cleanup

* clarify variable names

* remove logs query specific resource field component

* add url_builder test

* add azure_monitor_datasources tests

* address comments

* add types

* pass resourceUri to resource field component

* add test to check we reset the query fields when changing resources
This commit is contained in:
Kevin Yu 2022-04-08 08:49:46 -07:00 committed by GitHub
parent e3b123c3fc
commit ebe5f3646f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1668 additions and 675 deletions

View File

@ -217,7 +217,6 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo types
azlog.Debug("Failed to create request", "error", err) azlog.Debug("Failed to create request", "error", err)
return nil, errutil.Wrap("Failed to create request", err) return nil, errutil.Wrap("Failed to create request", err)
} }
req.URL.Path = "/subscriptions"
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
return req, nil return req, nil

View File

@ -163,7 +163,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
} }
azureMonitorQuery := &types.AzureMonitorQuery{ azureMonitorQuery := &types.AzureMonitorQuery{
URL: "12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics", URL: "/subscriptions/12345678-aaaa-bbbb-cccc-123456789abc/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana/providers/microsoft.insights/metrics",
UrlComponents: map[string]string{ UrlComponents: map[string]string{
"metricDefinition": "Microsoft.Compute/virtualMachines", "metricDefinition": "Microsoft.Compute/virtualMachines",
"resourceGroup": "grafanastaging", "resourceGroup": "grafanastaging",
@ -587,7 +587,7 @@ func TestAzureMonitorCreateRequest(t *testing.T) {
}{ }{
{ {
name: "creates a request", name: "creates a request",
expectedURL: "http://ds/subscriptions", expectedURL: "http://ds/",
expectedHeaders: http.Header{ expectedHeaders: http.Header{
"Content-Type": []string{"application/json"}, "Content-Type": []string{"application/json"},
}, },

View File

@ -18,6 +18,35 @@ type urlBuilder struct {
ResourceName string ResourceName string
} }
func (params *urlBuilder) buildMetricsURLFromLegacyQuery() string {
subscription := params.Subscription
if params.Subscription == "" {
subscription = params.DefaultSubscription
}
metricDefinitionArray := strings.Split(params.MetricDefinition, "/")
resourceNameArray := strings.Split(params.ResourceName, "/")
provider := metricDefinitionArray[0]
metricDefinitionArray = metricDefinitionArray[1:]
urlArray := []string{
"/subscriptions",
subscription,
"resourceGroups",
params.ResourceGroup,
"providers",
provider,
}
for i, metricDefinition := range metricDefinitionArray {
urlArray = append(urlArray, metricDefinition, resourceNameArray[i])
}
resourceURI := strings.Join(urlArray, "/")
return resourceURI
}
// BuildMetricsURL checks the metric definition property to see which form of the url // BuildMetricsURL checks the metric definition property to see which form of the url
// should be returned // should be returned
func (params *urlBuilder) BuildMetricsURL() string { func (params *urlBuilder) BuildMetricsURL() string {
@ -25,31 +54,7 @@ func (params *urlBuilder) BuildMetricsURL() string {
// Prior to Grafana 9, we had a legacy query object rather than a resourceURI, so we manually create the resource URI // Prior to Grafana 9, we had a legacy query object rather than a resourceURI, so we manually create the resource URI
if resourceURI == "" { if resourceURI == "" {
subscription := params.Subscription resourceURI = params.buildMetricsURLFromLegacyQuery()
if params.Subscription == "" {
subscription = params.DefaultSubscription
}
metricDefinitionArray := strings.Split(params.MetricDefinition, "/")
resourceNameArray := strings.Split(params.ResourceName, "/")
provider := metricDefinitionArray[0]
metricDefinitionArray = metricDefinitionArray[1:]
urlArray := []string{
subscription,
"resourceGroups",
params.ResourceGroup,
"providers",
provider,
}
for i := range metricDefinitionArray {
urlArray = append(urlArray, metricDefinitionArray[i])
urlArray = append(urlArray, resourceNameArray[i])
}
resourceURI = strings.Join(urlArray[:], "/")
} }
return fmt.Sprintf("%s/providers/microsoft.insights/metrics", resourceURI) return fmt.Sprintf("%s/providers/microsoft.insights/metrics", resourceURI)

View File

@ -3,23 +3,23 @@ package metrics
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/assert"
) )
func TestURLBuilder(t *testing.T) { func TestURLBuilder(t *testing.T) {
t.Run("AzureMonitor URL Builder", func(t *testing.T) { t.Run("AzureMonitor URL Builder", func(t *testing.T) {
t.Run("when only resource uri is provided it returns resource/uri/providers/microsoft.insights/metrics", func(t *testing.T) { t.Run("when only resource uri is provided it returns resource/uri/providers/microsoft.insights/metrics", func(t *testing.T) {
ub := &urlBuilder{ ub := &urlBuilder{
ResourceURI: "resource/uri", ResourceURI: "/subscriptions/sub/resource/uri",
} }
url := ub.BuildMetricsURL() url := ub.BuildMetricsURL()
require.Equal(t, url, "resource/uri/providers/microsoft.insights/metrics") assert.Equal(t, "/subscriptions/sub/resource/uri/providers/microsoft.insights/metrics", url)
}) })
t.Run("when resource uri and legacy fields are provided the legacy fields are ignored", func(t *testing.T) { t.Run("when resource uri and legacy fields are provided the legacy fields are ignored", func(t *testing.T) {
ub := &urlBuilder{ ub := &urlBuilder{
ResourceURI: "resource/uri", ResourceURI: "/subscriptions/sub/resource/uri",
DefaultSubscription: "default-sub", DefaultSubscription: "default-sub",
ResourceGroup: "rg", ResourceGroup: "rg",
MetricDefinition: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes", MetricDefinition: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes",
@ -27,7 +27,7 @@ func TestURLBuilder(t *testing.T) {
} }
url := ub.BuildMetricsURL() url := ub.BuildMetricsURL()
require.Equal(t, url, "resource/uri/providers/microsoft.insights/metrics") assert.Equal(t, "/subscriptions/sub/resource/uri/providers/microsoft.insights/metrics", url)
}) })
t.Run("Legacy URL Builder params", func(t *testing.T) { t.Run("Legacy URL Builder params", func(t *testing.T) {
@ -40,7 +40,7 @@ func TestURLBuilder(t *testing.T) {
} }
url := ub.BuildMetricsURL() url := ub.BuildMetricsURL()
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics") assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics", url)
}) })
t.Run("when metric definition is in the short form and a subscription is defined", func(t *testing.T) { t.Run("when metric definition is in the short form and a subscription is defined", func(t *testing.T) {
@ -53,7 +53,7 @@ func TestURLBuilder(t *testing.T) {
} }
url := ub.BuildMetricsURL() url := ub.BuildMetricsURL()
require.Equal(t, url, "specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics") assert.Equal(t, "/subscriptions/specified-sub/resourceGroups/rg/providers/Microsoft.Compute/virtualMachines/rn/providers/microsoft.insights/metrics", url)
}) })
t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) { t.Run("when metric definition is Microsoft.Storage/storageAccounts/blobServices", func(t *testing.T) {
@ -65,7 +65,7 @@ func TestURLBuilder(t *testing.T) {
} }
url := ub.BuildMetricsURL() url := ub.BuildMetricsURL()
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics") assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/providers/microsoft.insights/metrics", url)
}) })
t.Run("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func(t *testing.T) { t.Run("when metric definition is Microsoft.Storage/storageAccounts/fileServices", func(t *testing.T) {
@ -77,7 +77,7 @@ func TestURLBuilder(t *testing.T) {
} }
url := ub.BuildMetricsURL() url := ub.BuildMetricsURL()
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics") assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/providers/microsoft.insights/metrics", url)
}) })
t.Run("when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes", func(t *testing.T) { t.Run("when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes", func(t *testing.T) {
@ -89,7 +89,7 @@ func TestURLBuilder(t *testing.T) {
} }
url := ub.BuildMetricsURL() url := ub.BuildMetricsURL()
require.Equal(t, url, "default-sub/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/providers/microsoft.insights/metrics") assert.Equal(t, "/subscriptions/default-sub/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/providers/microsoft.insights/metrics", url)
}) })
}) })
}) })

View File

@ -121,9 +121,12 @@ type AzureMonitorJSONQuery struct {
// Legecy "resource" fields from before the resource picker provided just a single ResourceURI // Legecy "resource" fields from before the resource picker provided just a single ResourceURI
// These are used for pre-resource picker queries to reconstruct a resource URI // These are used for pre-resource picker queries to reconstruct a resource URI
// Deprecated
MetricDefinition string `json:"metricDefinition"` MetricDefinition string `json:"metricDefinition"`
ResourceGroup string `json:"resourceGroup"` // Deprecated
ResourceName string `json:"resourceName"` ResourceGroup string `json:"resourceGroup"`
// Deprecated
ResourceName string `json:"resourceName"`
AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"` AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"`
Dimension string `json:"dimension"` // old model Dimension string `json:"dimension"` // old model

View File

@ -16,6 +16,14 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
}, },
getSubscriptions: jest.fn().mockResolvedValueOnce([]), getSubscriptions: jest.fn().mockResolvedValueOnce([]),
defaultSubscriptionId: 'subscriptionId', defaultSubscriptionId: 'subscriptionId',
newGetMetricNamespaces: jest.fn().mockResolvedValueOnce([]),
newGetMetricNames: jest.fn().mockResolvedValueOnce([]),
newGetMetricMetadata: jest.fn().mockResolvedValueOnce({
primaryAggType: 'Average',
supportedAggTypes: ['Average', 'Maximum', 'Minimum'],
supportedTimeGrains: [],
dimensions: [],
}),
}, },
getAzureLogAnalyticsWorkspaces: jest.fn().mockResolvedValueOnce([]), getAzureLogAnalyticsWorkspaces: jest.fn().mockResolvedValueOnce([]),
@ -41,6 +49,7 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]), getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]),
getResourcesForResourceGroup: jest.fn().mockResolvedValue([]), getResourcesForResourceGroup: jest.fn().mockResolvedValue([]),
getResourceURIFromWorkspace: jest.fn().mockReturnValue(''), getResourceURIFromWorkspace: jest.fn().mockReturnValue(''),
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
}, },
...overrides, ...overrides,
}; };

View File

@ -17,6 +17,8 @@ export default function createMockQuery(): AzureMonitorQuery {
azureMonitor: { azureMonitor: {
// aggOptions: [], // aggOptions: [],
resourceUri:
'/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
aggregation: 'Average', aggregation: 'Average',
allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000], allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
// dimensionFilter: '*', // dimensionFilter: '*',

View File

@ -10,6 +10,7 @@ export default function createMockResourcePickerData(overrides?: DeepPartial<Res
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]), getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]),
getResourcesForResourceGroup: jest.fn().mockResolvedValue([]), getResourcesForResourceGroup: jest.fn().mockResolvedValue([]),
getResourceURIFromWorkspace: jest.fn().mockReturnValue(''), getResourceURIFromWorkspace: jest.fn().mockReturnValue(''),
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
...overrides, ...overrides,
}; };

View File

@ -31,7 +31,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{ {
id: 'dev-1', id: 'dev-1',
uri: '/subscriptions/def-456/resourceGroups/dev-1', uri: '/subscriptions/def-456/resourceGroups/dev-1',
name: 'Development', name: 'Development 1',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
children: [], children: [],
@ -39,7 +39,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{ {
id: 'dev-2', id: 'dev-2',
uri: '/subscriptions/def-456/resourceGroups/dev-2', uri: '/subscriptions/def-456/resourceGroups/dev-2',
name: 'Development', name: 'Development 2',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
children: [], children: [],
@ -55,7 +55,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{ {
id: 'dev-4', id: 'dev-4',
uri: '/subscriptions/def-456/resourceGroups/dev-4', uri: '/subscriptions/def-456/resourceGroups/dev-4',
name: 'Development', name: 'Development 3',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
children: [], children: [],
@ -63,7 +63,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{ {
id: 'dev-5', id: 'dev-5',
uri: '/subscriptions/def-456/resourceGroups/dev-5', uri: '/subscriptions/def-456/resourceGroups/dev-5',
name: 'Development', name: 'Development 4',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
children: [], children: [],

View File

@ -75,266 +75,59 @@ describe('AzureMonitorDatasource', () => {
}); });
}); });
}); });
describe('When performing getSubscriptions', () => {
describe('When performing newGetMetricNamespaces', () => {
const response = { const response = {
value: [ value: [
{ {
id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572', id: '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1/providers/microsoft.insights/metricNamespaces/Azure.ApplicationInsights',
subscriptionId: '99999999-cccc-bbbb-aaaa-9106972f9572', name: 'Azure.ApplicationInsights',
tenantId: '99999999-aaaa-bbbb-cccc-51c4f982ec48', type: 'Microsoft.Insights/metricNamespaces',
displayName: 'Primary Subscription', classification: 'Custom',
state: 'Enabled', properties: {
subscriptionPolicies: { metricNamespaceName: 'Azure.ApplicationInsights',
locationPlacementId: 'Public_2014-09-01',
quotaId: 'PayAsYouGo_2014-09-01',
spendingLimit: 'Off',
}, },
authorizationSource: 'RoleBased',
},
],
count: {
type: 'Total',
value: 1,
},
};
beforeEach(() => {
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
});
it('should return list of subscriptions', () => {
return ctx.ds.getSubscriptions().then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Primary Subscription');
expect(results[0].value).toEqual('99999999-cccc-bbbb-aaaa-9106972f9572');
});
});
});
describe('When performing getResourceGroups', () => {
const response = {
value: [{ name: 'grp1' }, { name: 'grp2' }],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
});
it('should return list of Resource Groups', () => {
return ctx.ds.getResourceGroups('subscriptionId').then((results: Array<{ text: string; value: string }>) => {
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 = {
value: [
{
name: 'test',
type: 'Microsoft.Network/networkInterfaces',
}, },
{ {
location: 'northeurope', id: '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1/providers/microsoft.insights/metricNamespaces/microsoft.insights-components',
name: 'northeur', name: 'microsoft.insights-components',
type: 'Microsoft.Compute/virtualMachines', type: 'Microsoft.Insights/metricNamespaces',
}, classification: 'Platform',
{ properties: {
location: 'westcentralus', metricNamespaceName: 'microsoft.insights/components',
name: 'us', },
type: 'Microsoft.Compute/virtualMachines',
},
{
name: 'IHaveNoMetrics',
type: 'IShouldBeFilteredOut',
},
{
name: 'storageTest',
type: 'Microsoft.Storage/storageAccounts',
}, },
], ],
}; };
beforeEach(() => { beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups'; const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
expect(path).toBe(basePath + '/nodesapp/resources?api-version=2021-04-01'); const expected =
basePath +
'/providers/microsoft.insights/components/resource1' +
'/providers/microsoft.insights/metricNamespaces?api-version=2017-12-01-preview';
expect(path).toBe(expected);
return Promise.resolve(response); return Promise.resolve(response);
}); });
}); });
it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => { it('should return list of Metric Namspaces', () => {
return ctx.ds return ctx.ds.azureMonitorDatasource
.getMetricDefinitions('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodesapp') .newGetMetricNamespaces(
'/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1'
)
.then((results: Array<{ text: string; value: string }>) => { .then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(7); expect(results.length).toEqual(2);
expect(results[0].text).toEqual('Network interface'); expect(results[0].text).toEqual('Azure.ApplicationInsights');
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces'); expect(results[0].value).toEqual('Azure.ApplicationInsights');
expect(results[1].text).toEqual('Virtual machine'); expect(results[1].text).toEqual('microsoft.insights-components');
expect(results[1].value).toEqual('Microsoft.Compute/virtualMachines'); expect(results[1].value).toEqual('microsoft.insights/components');
expect(results[2].text).toEqual('Storage account');
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('When performing newGetMetricNames', () => {
let subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
let resourceGroup = 'nodeapp';
let metricDefinition = 'microsoft.insights/components';
beforeEach(() => {
subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
resourceGroup = 'nodeapp';
metricDefinition = 'microsoft.insights/components';
});
describe('and there are no special cases', () => {
const response = {
value: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
},
{
name: resourceGroup,
type: metricDefinition,
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
`${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
);
return Promise.resolve(response);
});
});
it('should return list of Resource Names', () => {
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
});
});
it('should return ignore letter case', () => {
metricDefinition = 'microsoft.insights/Components';
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
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 = {
value: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
},
{
name: 'storagetest',
type: 'Microsoft.Storage/storageAccounts',
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
basePath +
`/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
);
return Promise.resolve(response);
});
});
it('should return list of Resource Names', () => {
metricDefinition = 'Microsoft.Storage/storageAccounts/blobServices';
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('storagetest/default');
expect(results[0].value).toEqual('storagetest/default');
});
});
});
describe('and there are several pages', () => {
const skipToken = 'token';
const response1 = {
value: [
{
name: `${resourceGroup}1`,
type: metricDefinition,
},
],
nextLink: `https://management.azure.com/resourceuri?$skiptoken=${skipToken}`,
};
const response2 = {
value: [
{
name: `${resourceGroup}2`,
type: metricDefinition,
},
],
};
beforeEach(() => {
const fn = jest.fn();
ctx.ds.azureMonitorDatasource.getResource = fn;
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
const expectedPath = `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`;
// first page
fn.mockImplementationOnce((path: string) => {
expect(path).toBe(expectedPath);
return Promise.resolve(response1);
});
// second page
fn.mockImplementationOnce((path: string) => {
expect(path).toBe(`${expectedPath}&$skiptoken=${skipToken}`);
return Promise.resolve(response2);
});
});
it('should return list of Resource Names', () => {
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(2);
expect(results[0].value).toEqual(`${resourceGroup}1`);
expect(results[1].value).toEqual(`${resourceGroup}2`);
});
});
});
});
describe('When performing getMetricNames', () => {
const response = { const response = {
value: [ value: [
{ {
@ -383,12 +176,9 @@ describe('AzureMonitorDatasource', () => {
}); });
it('should return list of Metric Definitions', () => { it('should return list of Metric Definitions', () => {
return ctx.ds return ctx.ds.azureMonitorDatasource
.getMetricNames( .newGetMetricNames(
'9935389e-9122-4ef9-95f9-1513dd24753f', '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1',
'nodeapp',
'microsoft.insights/components',
'resource1',
'default' 'default'
) )
.then((results: Array<{ text: string; value: string }>) => { .then((results: Array<{ text: string; value: string }>) => {
@ -401,7 +191,7 @@ describe('AzureMonitorDatasource', () => {
}); });
}); });
describe('When performing getMetricMetadata', () => { describe('When performing newGetMetricMetadata', () => {
const response = { const response = {
value: [ value: [
{ {
@ -450,12 +240,9 @@ describe('AzureMonitorDatasource', () => {
}); });
it('should return Aggregation metadata for a Metric', () => { it('should return Aggregation metadata for a Metric', () => {
return ctx.ds return ctx.ds.azureMonitorDatasource
.getMetricMetadata( .newGetMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f', '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1',
'nodeapp',
'microsoft.insights/components',
'resource1',
'default', 'default',
'UsedCapacity' 'UsedCapacity'
) )
@ -467,130 +254,524 @@ describe('AzureMonitorDatasource', () => {
}); });
}); });
describe('When performing getMetricMetadata on metrics with dimensions', () => { describe('Legacy Azure Monitor Query Object data fetchers', () => {
const response = { describe('When performing getSubscriptions', () => {
value: [ const response = {
{ value: [
name: { {
value: 'Transactions', id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
localizedValue: 'Transactions', subscriptionId: '99999999-cccc-bbbb-aaaa-9106972f9572',
tenantId: '99999999-aaaa-bbbb-cccc-51c4f982ec48',
displayName: 'Primary Subscription',
state: 'Enabled',
subscriptionPolicies: {
locationPlacementId: 'Public_2014-09-01',
quotaId: 'PayAsYouGo_2014-09-01',
spendingLimit: 'Off',
},
authorizationSource: 'RoleBased',
}, },
unit: 'Count', ],
primaryAggregationType: 'Total', count: {
supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'], type: 'Total',
isDimensionRequired: false, value: 1,
dimensions: [ },
};
beforeEach(() => {
ctx.instanceSettings.jsonData.azureAuthType = 'msi';
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
});
it('should return list of subscriptions', () => {
return ctx.ds.getSubscriptions().then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('Primary Subscription');
expect(results[0].value).toEqual('99999999-cccc-bbbb-aaaa-9106972f9572');
});
});
});
describe('When performing getResourceGroups', () => {
const response = {
value: [{ name: 'grp1' }, { name: 'grp2' }],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockResolvedValue(response);
});
it('should return list of Resource Groups', () => {
return ctx.ds.getResourceGroups('subscriptionId').then((results: Array<{ text: string; value: string }>) => {
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 = {
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',
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups';
expect(path).toBe(basePath + '/nodesapp/resources?api-version=2021-04-01');
return Promise.resolve(response);
});
});
it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
return ctx.ds
.getMetricDefinitions('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodesapp')
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(7);
expect(results[0].text).toEqual('Network interface');
expect(results[0].value).toEqual('Microsoft.Network/networkInterfaces');
expect(results[1].text).toEqual('Virtual machine');
expect(results[1].value).toEqual('Microsoft.Compute/virtualMachines');
expect(results[2].text).toEqual('Storage account');
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', () => {
let subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
let resourceGroup = 'nodeapp';
let metricDefinition = 'microsoft.insights/components';
beforeEach(() => {
subscription = '9935389e-9122-4ef9-95f9-1513dd24753f';
resourceGroup = 'nodeapp';
metricDefinition = 'microsoft.insights/components';
});
describe('and there are no special cases', () => {
const response = {
value: [
{ {
value: 'ResponseType', name: 'Failure Anomalies - nodeapp',
localizedValue: 'Response type', type: 'microsoft.insights/alertrules',
}, },
{ {
value: 'GeoType', name: resourceGroup,
localizedValue: 'Geo type', type: metricDefinition,
},
{
value: 'ApiName',
localizedValue: 'API name',
}, },
], ],
}, };
{
name: { beforeEach(() => {
value: 'FreeCapacity', ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
localizedValue: 'Free capacity', const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
`${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
);
return Promise.resolve(response);
});
});
it('should return list of Resource Names', () => {
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('nodeapp');
expect(results[0].value).toEqual('nodeapp');
});
});
it('should return ignore letter case', () => {
metricDefinition = 'microsoft.insights/Components';
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
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 = {
value: [
{
name: 'Failure Anomalies - nodeapp',
type: 'microsoft.insights/alertrules',
},
{
name: 'storagetest',
type: 'Microsoft.Storage/storageAccounts',
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
basePath +
`/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`
);
return Promise.resolve(response);
});
});
it('should return list of Resource Names', () => {
metricDefinition = 'Microsoft.Storage/storageAccounts/blobServices';
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(1);
expect(results[0].text).toEqual('storagetest/default');
expect(results[0].value).toEqual('storagetest/default');
});
});
});
describe('and there are several pages', () => {
const skipToken = 'token';
const response1 = {
value: [
{
name: `${resourceGroup}1`,
type: metricDefinition,
},
],
nextLink: `https://management.azure.com/resourceuri?$skiptoken=${skipToken}`,
};
const response2 = {
value: [
{
name: `${resourceGroup}2`,
type: metricDefinition,
},
],
};
beforeEach(() => {
const fn = jest.fn();
ctx.ds.azureMonitorDatasource.getResource = fn;
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
const expectedPath = `${basePath}/${resourceGroup}/resources?$filter=resourceType eq '${metricDefinition}'&api-version=2021-04-01`;
// first page
fn.mockImplementationOnce((path: string) => {
expect(path).toBe(expectedPath);
return Promise.resolve(response1);
});
// second page
fn.mockImplementationOnce((path: string) => {
expect(path).toBe(`${expectedPath}&$skiptoken=${skipToken}`);
return Promise.resolve(response2);
});
});
it('should return list of Resource Names', () => {
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricDefinition)
.then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(2);
expect(results[0].value).toEqual(`${resourceGroup}1`);
expect(results[1].value).toEqual(`${resourceGroup}2`);
});
});
});
});
describe('When performing getMetricNames', () => {
const response = {
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' },
],
}, },
unit: 'CountPerSecond', {
primaryAggregationType: 'Average', name: {
supportedAggregationTypes: ['None', 'Average'], 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' },
],
},
],
};
beforeEach(() => { beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => { ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp'; const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected = const expected =
basePath + basePath +
'/providers/microsoft.insights/components/resource1' + '/providers/microsoft.insights/components/resource1' +
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default'; '/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(path).toBe(expected); expect(path).toBe(expected);
return Promise.resolve(response); return Promise.resolve(response);
});
});
it('should return dimensions for a Metric that has dimensions', () => {
return ctx.ds
.getMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'microsoft.insights/components',
'resource1',
'default',
'Transactions'
)
.then((results: any) => {
expect(results.dimensions).toMatchInlineSnapshot(`
Array [
Object {
"label": "Response type",
"value": "ResponseType",
},
Object {
"label": "Geo type",
"value": "GeoType",
},
Object {
"label": "API name",
"value": "ApiName",
},
]
`);
}); });
});
describe('When performing targetContainsTemplate', () => {
it('should return false when no variable is being used', () => {
const query = createMockQuery();
query.queryType = AzureQueryType.AzureMonitor;
expect(ctx.ds.targetContainsTemplate(query)).toEqual(false);
}); });
it('should return true when subscriptions field is using a variable', () => { it('should return list of Metric Definitions', () => {
const query = createMockQuery(); return ctx.ds
const templateSrv = new TemplateSrv(); .getMetricNames(
templateSrv.init([subscriptionsVariable]); '9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv); 'microsoft.insights/components',
query.queryType = AzureQueryType.AzureMonitor; 'resource1',
query.subscription = `$${subscriptionsVariable.name}`; 'default'
expect(ds.targetContainsTemplate(query)).toEqual(true); )
}); .then((results: Array<{ text: string; value: string }>) => {
expect(results.length).toEqual(2);
it('should return false when a variable is used in a different part of the query', () => { expect(results[0].text).toEqual('Used capacity');
const query = createMockQuery(); expect(results[0].value).toEqual('UsedCapacity');
const templateSrv = new TemplateSrv(); expect(results[1].text).toEqual('Free capacity');
templateSrv.init([singleVariable]); expect(results[1].value).toEqual('FreeCapacity');
});
const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
query.queryType = AzureQueryType.AzureMonitor;
query.azureLogAnalytics = { resource: `$${singleVariable.name}` };
expect(ds.targetContainsTemplate(query)).toEqual(false);
}); });
}); });
it('should return an empty array for a Metric that does not have dimensions', () => { describe('When performing getMetricMetadata', () => {
return ctx.ds const response = {
.getMetricMetadata( value: [
'9935389e-9122-4ef9-95f9-1513dd24753f', {
'nodeapp', name: {
'microsoft.insights/components', value: 'UsedCapacity',
'resource1', localizedValue: 'Used capacity',
'default', },
'FreeCapacity' unit: 'CountPerSecond',
) primaryAggregationType: 'Total',
.then((results: any) => { supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
expect(results.dimensions.length).toEqual(0); 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' },
],
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
basePath +
'/providers/microsoft.insights/components/resource1' +
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(path).toBe(expected);
return Promise.resolve(response);
}); });
});
it('should return Aggregation metadata for a Metric', () => {
return ctx.ds
.getMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'microsoft.insights/components',
'resource1',
'default',
'UsedCapacity'
)
.then((results) => {
expect(results.primaryAggType).toEqual('Total');
expect(results.supportedAggTypes.length).toEqual(6);
expect(results.supportedTimeGrains.length).toEqual(5); // 4 time grains from the API + auto
});
});
});
describe('When performing getMetricMetadata on metrics with dimensions', () => {
const response = {
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'],
},
],
};
beforeEach(() => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
const expected =
basePath +
'/providers/microsoft.insights/components/resource1' +
'/providers/microsoft.insights/metricdefinitions?api-version=2018-01-01&metricnamespace=default';
expect(path).toBe(expected);
return Promise.resolve(response);
});
});
it('should return dimensions for a Metric that has dimensions', () => {
return ctx.ds
.getMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'microsoft.insights/components',
'resource1',
'default',
'Transactions'
)
.then((results: any) => {
expect(results.dimensions).toMatchInlineSnapshot(`
Array [
Object {
"label": "Response type",
"value": "ResponseType",
},
Object {
"label": "Geo type",
"value": "GeoType",
},
Object {
"label": "API name",
"value": "ApiName",
},
]
`);
});
});
describe('When performing targetContainsTemplate', () => {
it('should return false when no variable is being used', () => {
const query = createMockQuery();
query.queryType = AzureQueryType.AzureMonitor;
expect(ctx.ds.targetContainsTemplate(query)).toEqual(false);
});
it('should return true when subscriptions field is using a variable', () => {
const query = createMockQuery();
const templateSrv = new TemplateSrv();
templateSrv.init([subscriptionsVariable]);
const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
query.queryType = AzureQueryType.AzureMonitor;
query.subscription = `$${subscriptionsVariable.name}`;
expect(ds.targetContainsTemplate(query)).toEqual(true);
});
it('should return false when a variable is used in a different part of the query', () => {
const query = createMockQuery();
const templateSrv = new TemplateSrv();
templateSrv.init([singleVariable]);
const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
query.queryType = AzureQueryType.AzureMonitor;
query.azureLogAnalytics = { resource: `$${singleVariable.name}` };
expect(ds.targetContainsTemplate(query)).toEqual(false);
});
});
it('should return an empty array for a Metric that does not have dimensions', () => {
return ctx.ds
.getMetricMetadata(
'9935389e-9122-4ef9-95f9-1513dd24753f',
'nodeapp',
'microsoft.insights/components',
'resource1',
'default',
'FreeCapacity'
)
.then((results: any) => {
expect(results.dimensions.length).toEqual(0);
});
});
}); });
}); });
}); });

View File

@ -9,6 +9,9 @@ import TimegrainConverter from '../time_grain_converter';
import { import {
AzureDataSourceJsonData, AzureDataSourceJsonData,
AzureMonitorMetricDefinitionsResponse, AzureMonitorMetricDefinitionsResponse,
AzureMonitorMetricNamespacesResponse,
AzureMonitorMetricNamesResponse,
AzureMonitorMetricsMetadataResponse,
AzureMonitorQuery, AzureMonitorQuery,
AzureMonitorResourceGroupsResponse, AzureMonitorResourceGroupsResponse,
AzureQueryType, AzureQueryType,
@ -21,6 +24,10 @@ import UrlBuilder from './url_builder';
const defaultDropdownValue = 'select'; const defaultDropdownValue = 'select';
function hasValue(item?: string) {
return !!(item && item !== defaultDropdownValue);
}
export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> { export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
apiVersion = '2018-01-01'; apiVersion = '2018-01-01';
apiPreviewVersion = '2017-12-01-preview'; apiPreviewVersion = '2017-12-01-preview';
@ -40,7 +47,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId; this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId;
const cloud = getAzureCloud(instanceSettings); const cloud = getAzureCloud(instanceSettings);
this.resourcePath = `${routeNames.azureMonitor}/subscriptions`; this.resourcePath = routeNames.azureMonitor;
this.supportedMetricNamespaces = new SupportedNamespaces(cloud).get(); this.supportedMetricNamespaces = new SupportedNamespaces(cloud).get();
this.azurePortalUrl = getAzurePortalUrl(cloud); this.azurePortalUrl = getAzurePortalUrl(cloud);
} }
@ -51,19 +58,17 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
} }
filterQuery(item: AzureMonitorQuery): boolean { filterQuery(item: AzureMonitorQuery): boolean {
const hasResourceUri = !!item?.azureMonitor?.resourceUri;
const hasLegacyQuery =
hasValue(item?.azureMonitor?.resourceGroup) &&
hasValue(item?.azureMonitor?.resourceName) &&
hasValue(item?.azureMonitor?.metricDefinition);
return !!( return !!(
item.hide !== true && item.hide !== true &&
item.azureMonitor && (hasResourceUri || hasLegacyQuery) &&
item.azureMonitor.resourceGroup && hasValue(item?.azureMonitor?.metricName) &&
item.azureMonitor.resourceGroup !== defaultDropdownValue && hasValue(item?.azureMonitor?.aggregation)
item.azureMonitor.resourceName &&
item.azureMonitor.resourceName !== defaultDropdownValue &&
item.azureMonitor.metricDefinition &&
item.azureMonitor.metricDefinition !== defaultDropdownValue &&
item.azureMonitor.metricName &&
item.azureMonitor.metricName !== defaultDropdownValue &&
item.azureMonitor.aggregation &&
item.azureMonitor.aggregation !== defaultDropdownValue
); );
} }
@ -82,6 +87,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
const templateSrv = getTemplateSrv(); const templateSrv = getTemplateSrv();
const resourceUri = templateSrv.replace(item.resourceUri, scopedVars);
const subscriptionId = templateSrv.replace(target.subscription || this.defaultSubscriptionId, scopedVars); const subscriptionId = templateSrv.replace(target.subscription || this.defaultSubscriptionId, scopedVars);
const resourceGroup = templateSrv.replace(item.resourceGroup, scopedVars); const resourceGroup = templateSrv.replace(item.resourceGroup, scopedVars);
const resourceName = templateSrv.replace(item.resourceName, scopedVars); const resourceName = templateSrv.replace(item.resourceName, scopedVars);
@ -107,6 +113,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
subscription: subscriptionId, subscription: subscriptionId,
queryType: AzureQueryType.AzureMonitor, queryType: AzureQueryType.AzureMonitor,
azureMonitor: { azureMonitor: {
resourceUri,
resourceGroup, resourceGroup,
resourceName, resourceName,
metricDefinition, metricDefinition,
@ -128,14 +135,14 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
return []; return [];
} }
return this.getResource(`${this.resourcePath}?api-version=2019-03-01`).then((result: any) => { return this.getResource(`${this.resourcePath}/subscriptions?api-version=2019-03-01`).then((result: any) => {
return ResponseParser.parseSubscriptions(result); return ResponseParser.parseSubscriptions(result);
}); });
} }
getResourceGroups(subscriptionId: string) { getResourceGroups(subscriptionId: string) {
return this.getResource( return this.getResource(
`${this.resourcePath}/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}` `${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
).then((result: AzureMonitorResourceGroupsResponse) => { ).then((result: AzureMonitorResourceGroupsResponse) => {
return ResponseParser.parseResponseValues(result, 'name', 'name'); return ResponseParser.parseResponseValues(result, 'name', 'name');
}); });
@ -143,7 +150,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
getMetricDefinitions(subscriptionId: string, resourceGroup: string) { getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
return this.getResource( return this.getResource(
`${this.resourcePath}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${this.listByResourceGroupApiVersion}` `${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/resources?api-version=${this.listByResourceGroupApiVersion}`
) )
.then((result: AzureMonitorMetricDefinitionsResponse) => { .then((result: AzureMonitorMetricDefinitionsResponse) => {
return ResponseParser.parseResponseValues(result, 'type', 'type'); return ResponseParser.parseResponseValues(result, 'type', 'type');
@ -196,7 +203,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string, skipToken?: string) { getResourceNames(subscriptionId: string, resourceGroup: string, metricDefinition: string, skipToken?: string) {
let url = let url =
`${this.resourcePath}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?` + `${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/resources?` +
`$filter=resourceType eq '${metricDefinition}'&` + `$filter=resourceType eq '${metricDefinition}'&` +
`api-version=${this.listByResourceGroupApiVersion}`; `api-version=${this.listByResourceGroupApiVersion}`;
if (skipToken) { if (skipToken) {
@ -244,6 +251,19 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
}); });
} }
newGetMetricNamespaces(resourceUri: string) {
const templateSrv = getTemplateSrv();
const url = UrlBuilder.newBuildAzureMonitorGetMetricNamespacesUrl(
this.resourcePath,
templateSrv.replace(resourceUri),
this.apiPreviewVersion
);
return this.getResource(url).then((result: AzureMonitorMetricNamespacesResponse) => {
return ResponseParser.parseResponseValues(result, 'name', 'properties.metricNamespaceName');
});
}
getMetricNames( getMetricNames(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
@ -266,6 +286,20 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
}); });
} }
newGetMetricNames(resourceUri: string, metricNamespace: string) {
const templateSrv = getTemplateSrv();
const url = UrlBuilder.newBuildAzureMonitorGetMetricNamesUrl(
this.resourcePath,
templateSrv.replace(resourceUri),
templateSrv.replace(metricNamespace),
this.apiVersion
);
return this.getResource(url).then((result: AzureMonitorMetricNamesResponse) => {
return ResponseParser.parseResponseValues(result, 'name.localizedValue', 'name.value');
});
}
getMetricMetadata( getMetricMetadata(
subscriptionId: string, subscriptionId: string,
resourceGroup: string, resourceGroup: string,
@ -289,6 +323,20 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
}); });
} }
newGetMetricMetadata(resourceUri: string, metricNamespace: string, metricName: string) {
const templateSrv = getTemplateSrv();
const url = UrlBuilder.newBuildAzureMonitorGetMetricNamesUrl(
this.resourcePath,
templateSrv.replace(resourceUri),
templateSrv.replace(metricNamespace),
this.apiVersion
);
return this.getResource(url).then((result: AzureMonitorMetricsMetadataResponse) => {
return ResponseParser.parseMetadata(result, metricName);
});
}
async testDatasource(): Promise<DatasourceValidationResult> { async testDatasource(): Promise<DatasourceValidationResult> {
const validationError = this.validateDatasource(); const validationError = this.validateDatasource();
if (validationError) { if (validationError) {
@ -296,7 +344,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
} }
try { try {
const url = `${this.resourcePath}?api-version=2019-03-01`; const url = `${this.resourcePath}/subscriptions?api-version=2019-03-01`;
return await this.getResource(url).then<DatasourceValidationResult>((response: any) => { return await this.getResource(url).then<DatasourceValidationResult>((response: any) => {
return { return {

View File

@ -1,180 +1,209 @@
import UrlBuilder from './url_builder'; import UrlBuilder from './url_builder';
describe('AzureMonitorUrlBuilder', () => { describe('AzureMonitorUrlBuilder', () => {
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => { describe('when a resource uri is provided', () => {
it('should build the getMetricNamespaces url in the even longer format', () => { it('builds a getMetricNamesnamespace url', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( const url = UrlBuilder.newBuildAzureMonitorGetMetricNamespacesUrl(
'', '',
'sub1', '/subscriptions/sub/resource-uri/resource',
'rg',
'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
'rn1/rn2/rn3',
'2017-05-01-preview' '2017-05-01-preview'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' + '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
); );
}); });
}); });
describe('when metric definition is Microsoft.Sql/servers/databases', () => { describe('when a resource uri and metric namespace is provided', () => {
it('should build the getMetricNamespaces url in the longer format', () => { it('builds a getMetricNames url', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl( const url = UrlBuilder.newBuildAzureMonitorGetMetricNamesUrl(
'', '',
'sub1', '/subscriptions/sub/resource-uri/resource',
'rg',
'Microsoft.Sql/servers/databases',
'rn1/rn2',
'2017-05-01-preview'
);
expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
);
});
});
describe('when metric definition is Microsoft.Sql/servers', () => {
it('should build the getMetricNamespaces url in the shorter format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
'',
'sub1',
'rg',
'Microsoft.Sql/servers', 'Microsoft.Sql/servers',
'rn',
'2017-05-01-preview' '2017-05-01-preview'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + '/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=Microsoft.Sql%2Fservers'
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
); );
}); });
}); });
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => { describe('Legacy query object', () => {
it('should build the getMetricNames url in the even longer format', () => { describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( it('should build the getMetricNamespaces url in the even longer format', () => {
'', const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
'sub1', '',
'rg', 'sub1',
'Microsoft.NetApp/netAppAccounts/capacityPools/volumes', 'rg',
'rn1/rn2/rn3', 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
'default', 'rn1/rn2/rn3',
'2017-05-01-preview' '2017-05-01-preview'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
); );
});
}); });
});
describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Sql/servers/databases', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNamespaces url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
'', '',
'sub1', 'sub1',
'rg', 'rg',
'Microsoft.Sql/servers/databases', 'Microsoft.Sql/servers/databases',
'rn1/rn2', 'rn1/rn2',
'default', '2017-05-01-preview'
'2017-05-01-preview' );
); expect(url).toBe(
expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' + 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' );
); });
}); });
});
describe('when metric definition is Microsoft.Sql/servers and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Sql/servers', () => {
it('should build the getMetricNames url in the shorter format', () => { it('should build the getMetricNamespaces url in the shorter format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
'', '',
'sub1', 'sub1',
'rg', 'rg',
'Microsoft.Sql/servers', 'Microsoft.Sql/servers',
'rn', 'rn',
'default', '2017-05-01-preview'
'2017-05-01-preview' );
); expect(url).toBe(
expect(url).toBe( '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' + 'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' );
); });
}); });
});
describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the even longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'', '',
'sub1', 'sub1',
'rg', 'rg',
'Microsoft.Storage/storageAccounts/blobServices', 'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
'rn1/default', 'rn1/rn2/rn3',
'default', 'default',
'2017-05-01-preview' '2017-05-01-preview'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
); );
});
}); });
});
describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'', '',
'sub1', 'sub1',
'rg', 'rg',
'Microsoft.Storage/storageAccounts/fileServices', 'Microsoft.Sql/servers/databases',
'rn1/default', 'rn1/rn2',
'default', 'default',
'2017-05-01-preview' '2017-05-01-preview'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
); );
});
}); });
});
describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Sql/servers and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the shorter format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'', '',
'sub1', 'sub1',
'rg', 'rg',
'Microsoft.Storage/storageAccounts/tableServices', 'Microsoft.Sql/servers',
'rn1/default', 'rn',
'default', 'default',
'2017-05-01-preview' '2017-05-01-preview'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
); );
});
}); });
});
describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices and the metricNamespace is default', () => { describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => { it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl( const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'', '',
'sub1', 'sub1',
'rg', 'rg',
'Microsoft.Storage/storageAccounts/queueServices', 'Microsoft.Storage/storageAccounts/blobServices',
'rn1/default', 'rn1/default',
'default', 'default',
'2017-05-01-preview' '2017-05-01-preview'
); );
expect(url).toBe( expect(url).toBe(
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' + '/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default' 'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
); );
});
});
describe('when metric definition is Microsoft.Storage/storageAccounts/fileServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/fileServices',
'rn1/default',
'default',
'2017-05-01-preview'
);
expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/fileServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
);
});
});
describe('when metric definition is Microsoft.Storage/storageAccounts/tableServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/tableServices',
'rn1/default',
'default',
'2017-05-01-preview'
);
expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/tableServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
);
});
});
describe('when metric definition is Microsoft.Storage/storageAccounts/queueServices and the metricNamespace is default', () => {
it('should build the getMetricNames url in the longer format', () => {
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
'',
'sub1',
'rg',
'Microsoft.Storage/storageAccounts/queueServices',
'rn1/default',
'default',
'2017-05-01-preview'
);
expect(url).toBe(
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
);
});
}); });
}); });
}); });

View File

@ -10,7 +10,7 @@ export default class UrlBuilder {
const metricDefinitionArray = metricDefinition.split('/'); const metricDefinitionArray = metricDefinition.split('/');
const resourceNameArray = resourceName.split('/'); const resourceNameArray = resourceName.split('/');
const provider = metricDefinitionArray.shift(); const provider = metricDefinitionArray.shift();
const urlArray = [baseUrl, subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider]; const urlArray = [baseUrl, 'subscriptions', subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider];
for (const i in metricDefinitionArray) { for (const i in metricDefinitionArray) {
urlArray.push(metricDefinitionArray[i]); urlArray.push(metricDefinitionArray[i]);
urlArray.push(resourceNameArray[i]); urlArray.push(resourceNameArray[i]);
@ -31,7 +31,7 @@ export default class UrlBuilder {
const metricDefinitionArray = metricDefinition.split('/'); const metricDefinitionArray = metricDefinition.split('/');
const resourceNameArray = resourceName.split('/'); const resourceNameArray = resourceName.split('/');
const provider = metricDefinitionArray.shift(); const provider = metricDefinitionArray.shift();
const urlArray = [baseUrl, subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider]; const urlArray = [baseUrl, 'subscriptions', subscriptionId, 'resourceGroups', resourceGroup, 'providers', provider];
for (const i in metricDefinitionArray) { for (const i in metricDefinitionArray) {
urlArray.push(metricDefinitionArray[i]); urlArray.push(metricDefinitionArray[i]);
urlArray.push(resourceNameArray[i]); urlArray.push(resourceNameArray[i]);
@ -42,4 +42,20 @@ export default class UrlBuilder {
`&metricnamespace=${encodeURIComponent(metricNamespace)}` `&metricnamespace=${encodeURIComponent(metricNamespace)}`
); );
} }
static newBuildAzureMonitorGetMetricNamespacesUrl(baseUrl: string, resourceUri: string, apiVersion: string) {
return `${baseUrl}${resourceUri}/providers/microsoft.insights/metricNamespaces?api-version=${apiVersion}`;
}
static newBuildAzureMonitorGetMetricNamesUrl(
baseUrl: string,
resourceUri: string,
metricNamespace: string,
apiVersion: string
) {
return (
`${baseUrl}${resourceUri}/providers/microsoft.insights/metricdefinitions?api-version=${apiVersion}` +
`&metricnamespace=${encodeURIComponent(metricNamespace)}`
);
}
} }

View File

@ -2,10 +2,12 @@ import React from 'react';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
import Datasource from '../../datasource'; import Datasource from '../../datasource';
import { Alert, InlineFieldRow } from '@grafana/ui'; import { Alert, InlineFieldRow } from '@grafana/ui';
import { ResourceRowType } from '../ResourcePicker/types';
import ResourceField from '../ResourceField';
import QueryField from './QueryField'; import QueryField from './QueryField';
import FormatAsField from './FormatAsField'; import FormatAsField from './FormatAsField';
import ResourceField from './ResourceField';
import useMigrations from './useMigrations'; import useMigrations from './useMigrations';
import { setResource } from './setQueryValue';
interface LogsQueryEditorProps { interface LogsQueryEditorProps {
query: AzureMonitorQuery; query: AzureMonitorQuery;
@ -38,6 +40,14 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
variableOptionGroup={variableOptionGroup} variableOptionGroup={variableOptionGroup}
onQueryChange={onChange} onQueryChange={onChange}
setError={setError} setError={setError}
selectableEntryTypes={[
ResourceRowType.Subscription,
ResourceRowType.ResourceGroup,
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
setResource={setResource}
resourceUri={query.azureLogAnalytics?.resource}
/> />
</InlineFieldRow> </InlineFieldRow>

View File

@ -6,7 +6,6 @@ import { AzureMetricQuery, AzureMonitorOption, AzureMonitorQuery, AzureQueryType
import { import {
DataHook, DataHook,
updateSubscriptions, updateSubscriptions,
useAsyncState,
useMetricNames, useMetricNames,
useMetricNamespaces, useMetricNamespaces,
useResourceGroups, useResourceGroups,
@ -15,75 +14,12 @@ import {
useSubscriptions, useSubscriptions,
} from './dataHooks'; } from './dataHooks';
interface WaitableMock extends jest.Mock<any, any> {
waitToBeCalled(): Promise<unknown>;
}
const WAIT_OPTIONS = { const WAIT_OPTIONS = {
timeout: 1000, timeout: 1000,
}; };
function createWaitableMock() {
let resolve: Function;
const mock = jest.fn() as WaitableMock;
mock.mockImplementation(() => {
resolve && resolve();
});
mock.waitToBeCalled = () => {
return new Promise((_resolve) => (resolve = _resolve));
};
return mock;
}
const opt = (text: string, value: string) => ({ text, value }); const opt = (text: string, value: string) => ({ text, value });
describe('AzureMonitor: useAsyncState', () => {
const MOCKED_RANDOM_VALUE = 0.42069;
beforeEach(() => {
jest.spyOn(global.Math, 'random').mockReturnValue(MOCKED_RANDOM_VALUE);
});
afterEach(() => {
jest.spyOn(global.Math, 'random').mockRestore();
});
it('should return data from an async function', async () => {
const apiCall = () => Promise.resolve(['a', 'b', 'c']);
const setError = jest.fn();
const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await waitForNextUpdate();
expect(result.current).toEqual(['a', 'b', 'c']);
});
it('should report errors through setError', async () => {
const error = new Error();
const apiCall = () => Promise.reject(error);
const setError = createWaitableMock();
const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
expect(result.current).toEqual([]);
expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, error);
});
it('should clear the error once the request is successful', async () => {
const apiCall = () => Promise.resolve(['a', 'b', 'c']);
const setError = createWaitableMock();
const { waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, undefined);
});
});
interface TestScenario { interface TestScenario {
name: string; name: string;
hook: DataHook; hook: DataHook;

View File

@ -1,8 +1,9 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useState } from 'react';
import Datasource from '../../datasource'; import Datasource from '../../datasource';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types'; import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
import { hasOption, toOption } from '../../utils/common'; import { hasOption, toOption } from '../../utils/common';
import { useAsyncState } from '../../utils/useAsyncState';
import { setMetricNamespace, setSubscriptionID } from './setQueryValue'; import { setMetricNamespace, setSubscriptionID } from './setQueryValue';
export interface MetricMetadata { export interface MetricMetadata {
@ -26,29 +27,6 @@ export type DataHook = (
setError: SetErrorFn setError: SetErrorFn
) => AzureMonitorOption[]; ) => AzureMonitorOption[];
export function useAsyncState<T>(asyncFn: () => Promise<T>, setError: Function, dependencies: unknown[]) {
// Use the lazy initial state functionality of useState to assign a random ID to the API call
// to track where errors come from. See useLastError.
const [errorSource] = useState(() => Math.random());
const [value, setValue] = useState<T>();
const finalValue = useMemo(() => value ?? [], [value]);
useEffect(() => {
asyncFn()
.then((results) => {
setValue(results);
setError(errorSource, undefined);
})
.catch((err) => {
setError(errorSource, err);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
return finalValue;
}
export const updateSubscriptions = ( export const updateSubscriptions = (
query: AzureMonitorQuery, query: AzureMonitorQuery,
subscriptionOptions: AzureMonitorOption[], subscriptionOptions: AzureMonitorOption[],

View File

@ -1,5 +1,20 @@
import { AzureMetricDimension, AzureMonitorQuery } from '../../types'; import { AzureMetricDimension, AzureMonitorQuery } from '../../types';
export function setResource(query: AzureMonitorQuery, resourceURI: string | undefined): AzureMonitorQuery {
return {
...query,
azureMonitor: {
...query.azureMonitor,
resourceUri: resourceURI,
metricNamespace: undefined,
metricName: undefined,
aggregation: undefined,
timeGrain: '',
dimensionFilters: [],
},
};
}
export function setSubscriptionID(query: AzureMonitorQuery, subscriptionID: string): AzureMonitorQuery { export function setSubscriptionID(query: AzureMonitorQuery, subscriptionID: string): AzureMonitorQuery {
if (query.subscription === subscriptionID) { if (query.subscription === subscriptionID) {
return query; return query;

View File

@ -0,0 +1,226 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectOptionInTest } from '@grafana/ui';
import MetricsQueryEditor from './MetricsQueryEditor';
import createMockQuery from '../../__mocks__/query';
import createMockDatasource from '../../__mocks__/datasource';
import createMockResourcePickerData from '../../__mocks__/resourcePickerData';
import {
createMockResourceGroupsBySubscription,
createMockSubscriptions,
mockResourcesByResourceGroup,
} from '../../__mocks__/resourcePickerRows';
const variableOptionGroup = {
label: 'Template variables',
options: [],
};
const resourcePickerData = createMockResourcePickerData({
getSubscriptions: jest.fn().mockResolvedValue(createMockSubscriptions()),
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue(createMockResourceGroupsBySubscription()),
getResourcesForResourceGroup: jest.fn().mockResolvedValue(mockResourcesByResourceGroup()),
});
describe('MetricsQueryEditor', () => {
const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = function () {};
});
afterEach(() => {
window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
});
it('should render', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData });
render(
<MetricsQueryEditor
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={() => {}}
setError={() => {}}
/>
);
expect(await screen.findByTestId('azure-monitor-metrics-query-editor-with-resource-picker')).toBeInTheDocument();
});
it('should change resource when a resource is selected in the ResourcePicker', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData });
const query = createMockQuery();
delete query?.azureMonitor?.resourceUri;
const onChange = jest.fn();
render(
<MetricsQueryEditor
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
expect(resourcePickerButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
expect(subscriptionButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand A Great Resource Group' })).not.toBeInTheDocument();
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
expect(resourceGroupButton).toBeInTheDocument();
expect(screen.queryByLabelText('web-server')).not.toBeInTheDocument();
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('web-server');
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeChecked();
userEvent.click(checkbox);
expect(checkbox).toBeChecked();
userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureMonitor: expect.objectContaining({
resourceUri:
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server',
}),
})
);
});
it('should reset metric namespace, metric name, and aggregation fields after selecting a new resource when a valid query has already been set', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData });
const query = createMockQuery();
const onChange = jest.fn();
render(
<MetricsQueryEditor
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: /grafanastaging/ });
expect(screen.getByText('Microsoft.Compute/virtualMachines')).toBeInTheDocument();
expect(screen.getByText('Metric A')).toBeInTheDocument();
expect(screen.getByText('Average')).toBeInTheDocument();
expect(resourcePickerButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Primary Subscription' })).not.toBeInTheDocument();
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Dev Subscription' });
expect(subscriptionButton).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Expand Development 3' })).not.toBeInTheDocument();
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand Development 3' });
expect(resourceGroupButton).toBeInTheDocument();
expect(screen.queryByLabelText('db-server')).not.toBeInTheDocument();
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('db-server');
expect(checkbox).toBeInTheDocument();
expect(checkbox).not.toBeChecked();
userEvent.click(checkbox);
expect(checkbox).toBeChecked();
userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureMonitor: expect.objectContaining({
resourceUri:
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server',
metricNamespace: undefined,
metricName: undefined,
aggregation: undefined,
timeGrain: '',
dimensionFilters: [],
}),
})
);
});
it('should change the metric name when selected', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData });
const onChange = jest.fn();
const mockQuery = createMockQuery();
mockDatasource.azureMonitorDatasource.newGetMetricNames = jest.fn().mockResolvedValue([
{
value: 'metric-a',
text: 'Metric A',
},
{
value: 'metric-b',
text: 'Metric B',
},
]);
render(
<MetricsQueryEditor
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const metrics = await screen.findByLabelText('Metric');
expect(metrics).toBeInTheDocument();
await selectOptionInTest(metrics, 'Metric B');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
metricName: 'metric-b',
aggregation: undefined,
timeGrain: '',
},
});
});
it('should change the aggregation type when selected', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData });
const onChange = jest.fn();
const mockQuery = createMockQuery();
render(
<MetricsQueryEditor
query={createMockQuery()}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const aggregation = await screen.findByLabelText('Aggregation');
expect(aggregation).toBeInTheDocument();
await selectOptionInTest(aggregation, 'Maximum');
expect(onChange).toHaveBeenLastCalledWith({
...mockQuery,
azureMonitor: {
...mockQuery.azureMonitor,
aggregation: 'Maximum',
},
});
});
});

View File

@ -1,9 +1,113 @@
import React from 'react'; import React from 'react';
import { InlineFieldRow } from '@grafana/ui';
import AggregationField from '../MetricsQueryEditor/AggregationField';
import MetricNameField from '../MetricsQueryEditor/MetricNameField';
import MetricNamespaceField from '../MetricsQueryEditor/MetricNamespaceField';
import TimeGrainField from '../MetricsQueryEditor/TimeGrainField';
import DimensionFields from '../MetricsQueryEditor/DimensionFields';
import TopField from '../MetricsQueryEditor/TopField';
import LegendFormatField from '../MetricsQueryEditor/LegendFormatField';
import ResourceField from '../ResourceField';
import { ResourceRowType } from '../ResourcePicker/types';
import type Datasource from '../../datasource';
import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types';
import { useMetricNames, useMetricNamespaces, useMetricMetadata } from './dataHooks';
import { setResource } from '../MetricsQueryEditor/setQueryValue';
interface MetricsQueryEditorProps {} interface MetricsQueryEditorProps {
query: AzureMonitorQuery;
datasource: Datasource;
onChange: (newQuery: AzureMonitorQuery) => void;
variableOptionGroup: { label: string; options: AzureMonitorOption[] };
setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
}
const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({}) => { const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
return <div data-testid="azure-monitor-metrics-query-editor-with-resource-picker">New Query Editor</div>; query,
datasource,
variableOptionGroup,
onChange,
setError,
}) => {
const metricsMetadata = useMetricMetadata(query, datasource, onChange);
const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError);
const metricNames = useMetricNames(query, datasource, onChange, setError);
return (
<div data-testid="azure-monitor-metrics-query-editor-with-resource-picker">
<InlineFieldRow>
<ResourceField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[ResourceRowType.Resource]}
setResource={setResource}
resourceUri={query.azureMonitor?.resourceUri}
/>
</InlineFieldRow>
<InlineFieldRow>
<MetricNamespaceField
metricNamespaces={metricNamespaces}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<MetricNameField
metricNames={metricNames}
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</InlineFieldRow>
<InlineFieldRow>
<AggregationField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
aggregationOptions={metricsMetadata?.aggOptions ?? []}
isLoading={metricsMetadata.isLoading}
/>
<TimeGrainField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
timeGrainOptions={metricsMetadata?.timeGrains ?? []}
/>
</InlineFieldRow>
<DimensionFields
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
dimensionOptions={metricsMetadata?.dimensions ?? []}
/>
<TopField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
<LegendFormatField
query={query}
datasource={datasource}
variableOptionGroup={variableOptionGroup}
onQueryChange={onChange}
setError={setError}
/>
</div>
);
}; };
export default MetricsQueryEditor; export default MetricsQueryEditor;

View File

@ -0,0 +1,165 @@
import { renderHook } from '@testing-library/react-hooks';
import createMockDatasource from '../../__mocks__/datasource';
import Datasource from '../../datasource';
import { AzureMetricQuery, AzureMonitorOption, AzureMonitorQuery, AzureQueryType } from '../../types';
import { DataHook } from '../MetricsQueryEditor/dataHooks';
import { useMetricNames, useMetricNamespaces } from './dataHooks';
const WAIT_OPTIONS = {
timeout: 1000,
};
const opt = (text: string, value: string) => ({ text, value });
interface TestScenario {
name: string;
hook: DataHook;
// For convenience, only need to define the azureMonitor part of the query for some tests
emptyQueryPartial: AzureMetricQuery;
customProperties: AzureMetricQuery;
topLevelCustomProperties?: Partial<AzureMonitorQuery>;
expectedCustomPropertyResults?: Array<AzureMonitorOption<string>>;
expectedOptions: AzureMonitorOption[];
}
describe('AzureMonitor: metrics dataHooks', () => {
const bareQuery = {
refId: 'A',
queryType: AzureQueryType.AzureMonitor,
subscription: 'sub-abc-123',
};
const testTable: TestScenario[] = [
{
name: 'useMetricNames',
hook: useMetricNames,
emptyQueryPartial: {
resourceUri:
'/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
metricNamespace: 'azure/vm',
},
customProperties: {
resourceUri:
'/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
metricNamespace: 'azure/vm',
metricName: 'metric-$ENVIRONMENT',
},
expectedOptions: [
{
label: 'Percentage CPU',
value: 'percentage-cpu',
},
{
label: 'Free memory',
value: 'free-memory',
},
],
expectedCustomPropertyResults: [
{ label: 'Percentage CPU', value: 'percentage-cpu' },
{ label: 'Free memory', value: 'free-memory' },
{ label: 'metric-$ENVIRONMENT', value: 'metric-$ENVIRONMENT' },
],
},
{
name: 'useMetricNamespaces',
hook: useMetricNamespaces,
emptyQueryPartial: {
resourceUri:
'/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
metricNamespace: 'azure/vm',
},
customProperties: {
resourceUri:
'/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
metricNamespace: 'azure/vm-$ENVIRONMENT',
metricName: 'metric-name',
},
expectedOptions: [
{
label: 'Compute Virtual Machine',
value: 'azure/vmc',
},
{
label: 'Database NS',
value: 'azure/dbns',
},
{
label: 'azure/vm',
value: 'azure/vm',
},
],
expectedCustomPropertyResults: [
{ label: 'Compute Virtual Machine', value: 'azure/vmc' },
{ label: 'Database NS', value: 'azure/dbns' },
{ label: 'azure/vm-$ENVIRONMENT', value: 'azure/vm-$ENVIRONMENT' },
],
},
];
let datasource: Datasource;
let onChange: jest.Mock<any, any>;
let setError: jest.Mock<any, any>;
beforeEach(() => {
onChange = jest.fn();
setError = jest.fn();
datasource = createMockDatasource();
datasource.getVariables = jest.fn().mockReturnValue(['$sub', '$rg', '$rt', '$variable']);
datasource.azureMonitorDatasource.getSubscriptions = jest
.fn()
.mockResolvedValue([opt('sub-abc-123', 'sub-abc-123')]);
datasource.getResourceGroups = jest
.fn()
.mockResolvedValue([
opt('Web App - Production', 'web-app-production'),
opt('Web App - Development', 'web-app-development'),
]);
datasource.getMetricDefinitions = jest
.fn()
.mockResolvedValue([opt('Virtual Machine', 'azure/vm'), opt('Database', 'azure/db')]);
datasource.getResourceNames = jest
.fn()
.mockResolvedValue([opt('Web server', 'web-server'), opt('Job server', 'job-server')]);
datasource.azureMonitorDatasource.newGetMetricNames = jest
.fn()
.mockResolvedValue([opt('Percentage CPU', 'percentage-cpu'), opt('Free memory', 'free-memory')]);
datasource.azureMonitorDatasource.newGetMetricNamespaces = jest
.fn()
.mockResolvedValue([opt('Compute Virtual Machine', 'azure/vmc'), opt('Database NS', 'azure/dbns')]);
});
describe.each(testTable)('scenario %#: $name', (scenario) => {
it('returns values', async () => {
const query = {
...bareQuery,
azureMonitor: scenario.emptyQueryPartial,
};
const { result, waitForNextUpdate } = renderHook(() => scenario.hook(query, datasource, onChange, setError));
await waitForNextUpdate(WAIT_OPTIONS);
expect(result.current).toEqual(scenario.expectedOptions);
});
it('adds custom properties as a valid option', async () => {
const query = {
...bareQuery,
azureMonitor: scenario.customProperties,
...scenario.topLevelCustomProperties,
};
const { result, waitForNextUpdate } = renderHook(() => scenario.hook(query, datasource, onChange, setError));
await waitForNextUpdate(WAIT_OPTIONS);
expect(result.current).toEqual(scenario.expectedCustomPropertyResults);
});
});
});

View File

@ -0,0 +1,144 @@
import { useEffect, useState } from 'react';
import Datasource from '../../datasource';
import { AzureMonitorOption, AzureMonitorQuery } from '../../types';
import { toOption } from '../../utils/common';
import { useAsyncState } from '../../utils/useAsyncState';
import { DataHook } from '../MetricsQueryEditor/dataHooks';
import { setMetricNamespace } from '../MetricsQueryEditor/setQueryValue';
export interface MetricMetadata {
aggOptions: AzureMonitorOption[];
timeGrains: AzureMonitorOption[];
dimensions: AzureMonitorOption[];
isLoading: boolean;
// These two properties are only used within the hook, and not elsewhere
supportedAggTypes: string[];
primaryAggType: string | undefined;
}
type OnChangeFn = (newQuery: AzureMonitorQuery) => void;
export const useMetricNamespaces: DataHook = (query, datasource, onChange, setError) => {
const { metricNamespace, resourceUri } = query.azureMonitor ?? {};
const metricNamespaces = useAsyncState(
async () => {
if (!resourceUri) {
return;
}
const results = await datasource.azureMonitorDatasource.newGetMetricNamespaces(resourceUri);
const options = formatOptions(results, metricNamespace);
// Do some cleanup of the query state if need be
if (!metricNamespace && options.length) {
onChange(setMetricNamespace(query, options[0].value));
}
return options;
},
setError,
[resourceUri]
);
return metricNamespaces;
};
export const useMetricNames: DataHook = (query, datasource, onChange, setError) => {
const { metricNamespace, metricName, resourceUri } = query.azureMonitor ?? {};
return useAsyncState(
async () => {
if (!(metricNamespace && resourceUri)) {
return;
}
const results = await datasource.azureMonitorDatasource.newGetMetricNames(resourceUri, metricNamespace);
const options = formatOptions(results, metricName);
return options;
},
setError,
[resourceUri, metricNamespace]
);
};
const defaultMetricMetadata: MetricMetadata = {
aggOptions: [],
timeGrains: [],
dimensions: [],
isLoading: false,
supportedAggTypes: [],
primaryAggType: undefined,
};
export const useMetricMetadata = (query: AzureMonitorQuery, datasource: Datasource, onChange: OnChangeFn) => {
const [metricMetadata, setMetricMetadata] = useState<MetricMetadata>(defaultMetricMetadata);
const { resourceUri, metricNamespace, metricName, aggregation, timeGrain } = query.azureMonitor ?? {};
// Fetch new metric metadata when the fields change
useEffect(() => {
if (!(resourceUri && metricNamespace && metricName)) {
setMetricMetadata(defaultMetricMetadata);
return;
}
datasource.azureMonitorDatasource
.newGetMetricMetadata(resourceUri, metricNamespace, metricName)
.then((metadata) => {
// TODO: Move the aggregationTypes and timeGrain defaults into `getMetricMetadata`
const aggregations = (metadata.supportedAggTypes || [metadata.primaryAggType]).map((v) => ({
label: v,
value: v,
}));
setMetricMetadata({
aggOptions: aggregations,
timeGrains: metadata.supportedTimeGrains,
dimensions: metadata.dimensions,
isLoading: false,
supportedAggTypes: metadata.supportedAggTypes ?? [],
primaryAggType: metadata.primaryAggType,
});
});
}, [datasource, resourceUri, metricNamespace, metricName]);
// Update the query state in response to the meta data changing
useEffect(() => {
const newAggregation = aggregation || metricMetadata.primaryAggType;
const newTimeGrain = timeGrain || 'auto';
if (newAggregation !== aggregation || newTimeGrain !== timeGrain) {
onChange({
...query,
azureMonitor: {
...query.azureMonitor,
aggregation: newAggregation,
timeGrain: newTimeGrain,
},
});
}
}, [onChange, metricMetadata, aggregation, timeGrain, query]);
return metricMetadata;
};
function formatOptions(
rawResults: Array<{
text: string;
value: string;
}>,
selectedValue?: string
) {
const options = rawResults.map(toOption);
// account for custom values that might have been set in json file like ones crafted with a template variable (ex: "cloud-datasource-resource-$Environment")
if (selectedValue && !options.find((option) => option.value === selectedValue)) {
options.push({ label: selectedValue, value: selectedValue });
}
return options;
}

View File

@ -99,7 +99,15 @@ const EditorForQueryType: React.FC<EditorForQueryTypeProps> = ({
switch (query.queryType) { switch (query.queryType) {
case AzureQueryType.AzureMonitor: case AzureQueryType.AzureMonitor:
if (config.featureToggles.azureMonitorResourcePickerForMetrics) { if (config.featureToggles.azureMonitorResourcePickerForMetrics) {
return <NewMetricsQueryEditor />; return (
<NewMetricsQueryEditor
query={query}
datasource={datasource}
onChange={onChange}
variableOptionGroup={variableOptionGroup}
setError={setError}
/>
);
} }
return ( return (
<MetricsQueryEditor <MetricsQueryEditor

View File

@ -4,13 +4,12 @@ import { Button, Icon, Modal, useStyles2 } from '@grafana/ui';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import Datasource from '../../datasource'; import Datasource from '../../datasource';
import { AzureQueryEditorFieldProps, AzureResourceSummaryItem } from '../../types'; import { AzureQueryEditorFieldProps, AzureMonitorQuery, AzureResourceSummaryItem } from '../../types';
import { Field } from '../Field'; import { Field } from '../Field';
import ResourcePicker from '../ResourcePicker'; import ResourcePicker from '../ResourcePicker';
import { ResourceRowType } from '../ResourcePicker/types'; import { ResourceRowType } from '../ResourcePicker/types';
import { parseResourceURI } from '../ResourcePicker/utils'; import { parseResourceURI } from '../ResourcePicker/utils';
import { Space } from '../Space'; import { Space } from '../Space';
import { setResource } from './setQueryValue';
function parseResourceDetails(resourceURI: string) { function parseResourceDetails(resourceURI: string) {
const parsed = parseResourceURI(resourceURI); const parsed = parseResourceURI(resourceURI);
@ -26,9 +25,21 @@ function parseResourceDetails(resourceURI: string) {
}; };
} }
const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, onQueryChange }) => { interface ResourceFieldProps extends AzureQueryEditorFieldProps {
setResource: (query: AzureMonitorQuery, resourceURI?: string) => AzureMonitorQuery;
selectableEntryTypes: ResourceRowType[];
resourceUri?: string;
}
const ResourceField: React.FC<ResourceFieldProps> = ({
query,
datasource,
onQueryChange,
setResource,
selectableEntryTypes,
resourceUri,
}) => {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const { resource } = query.azureLogAnalytics ?? {};
const [pickerIsOpen, setPickerIsOpen] = useState(false); const [pickerIsOpen, setPickerIsOpen] = useState(false);
const handleOpenPicker = useCallback(() => { const handleOpenPicker = useCallback(() => {
@ -44,7 +55,7 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
onQueryChange(setResource(query, resourceURI)); onQueryChange(setResource(query, resourceURI));
closePicker(); closePicker();
}, },
[closePicker, onQueryChange, query] [closePicker, onQueryChange, query, setResource]
); );
return ( return (
@ -60,21 +71,16 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
> >
<ResourcePicker <ResourcePicker
resourcePickerData={datasource.resourcePickerData} resourcePickerData={datasource.resourcePickerData}
resourceURI={resource} resourceURI={resourceUri}
onApply={handleApply} onApply={handleApply}
onCancel={closePicker} onCancel={closePicker}
selectableEntryTypes={[ selectableEntryTypes={selectableEntryTypes}
ResourceRowType.Subscription,
ResourceRowType.ResourceGroup,
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
/> />
</Modal> </Modal>
<Field label="Resource"> <Field label="Resource">
<Button variant="secondary" onClick={handleOpenPicker} type="button"> <Button variant="secondary" onClick={handleOpenPicker} type="button">
<ResourceLabel resource={resource} datasource={datasource} /> <ResourceLabel resource={resourceUri} datasource={datasource} />
</Button> </Button>
</Field> </Field>
</> </>

View File

@ -0,0 +1 @@
export { default } from './ResourceField';

View File

@ -36,6 +36,7 @@ export interface AzureMonitorQuery extends DeprecatedAzureMonitorQuery {
* Azure Monitor Metrics sub-query properties * Azure Monitor Metrics sub-query properties
*/ */
export interface AzureMetricQuery { export interface AzureMetricQuery {
resourceUri?: string;
resourceGroup?: string; resourceGroup?: string;
/** Resource type */ /** Resource type */

View File

@ -95,6 +95,23 @@ export interface AzureMonitorMetricMetadataItem {
metricAvailabilities?: AzureMonitorMetricAvailabilityMetadata[]; 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 { export interface AzureMonitorMetricAvailabilityMetadata {
timeGrain: string; timeGrain: string;
retention: string; retention: string;

View File

@ -0,0 +1,65 @@
import { renderHook } from '@testing-library/react-hooks';
import { useAsyncState } from './useAsyncState';
interface WaitableMock extends jest.Mock<any, any> {
waitToBeCalled(): Promise<unknown>;
}
function createWaitableMock() {
let resolve: Function;
const mock = jest.fn() as WaitableMock;
mock.mockImplementation(() => {
resolve && resolve();
});
mock.waitToBeCalled = () => {
return new Promise((_resolve) => (resolve = _resolve));
};
return mock;
}
describe('useAsyncState', () => {
const MOCKED_RANDOM_VALUE = 0.42069;
beforeEach(() => {
jest.spyOn(global.Math, 'random').mockReturnValue(MOCKED_RANDOM_VALUE);
});
afterEach(() => {
jest.spyOn(global.Math, 'random').mockRestore();
});
it('should return data from an async function', async () => {
const apiCall = () => Promise.resolve(['a', 'b', 'c']);
const setError = jest.fn();
const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await waitForNextUpdate();
expect(result.current).toEqual(['a', 'b', 'c']);
});
it('should report errors through setError', async () => {
const error = new Error();
const apiCall = () => Promise.reject(error);
const setError = createWaitableMock();
const { result, waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
expect(result.current).toEqual([]);
expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, error);
});
it('should clear the error once the request is successful', async () => {
const apiCall = () => Promise.resolve(['a', 'b', 'c']);
const setError = createWaitableMock();
const { waitForNextUpdate } = renderHook(() => useAsyncState(apiCall, setError, []));
await Promise.race([waitForNextUpdate(), setError.waitToBeCalled()]);
expect(setError).toHaveBeenCalledWith(MOCKED_RANDOM_VALUE, undefined);
});
});

View File

@ -0,0 +1,24 @@
import { useEffect, useMemo, useState } from 'react';
export function useAsyncState<T>(asyncFn: () => Promise<T>, setError: Function, dependencies: unknown[]) {
// Use the lazy initial state functionality of useState to assign a random ID to the API call
// to track where errors come from. See useLastError.
const [errorSource] = useState(() => Math.random());
const [value, setValue] = useState<T>();
const finalValue = useMemo(() => value ?? [], [value]);
useEffect(() => {
asyncFn()
.then((results) => {
setValue(results);
setError(errorSource, undefined);
})
.catch((err) => {
setError(errorSource, err);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, dependencies);
return finalValue;
}