mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
e3b123c3fc
commit
ebe5f3646f
@ -217,7 +217,6 @@ func (e *AzureMonitorDatasource) createRequest(ctx context.Context, dsInfo types
|
||||
azlog.Debug("Failed to create request", "error", err)
|
||||
return nil, errutil.Wrap("Failed to create request", err)
|
||||
}
|
||||
req.URL.Path = "/subscriptions"
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
return req, nil
|
||||
|
@ -163,7 +163,7 @@ func TestAzureMonitorBuildQueries(t *testing.T) {
|
||||
}
|
||||
|
||||
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{
|
||||
"metricDefinition": "Microsoft.Compute/virtualMachines",
|
||||
"resourceGroup": "grafanastaging",
|
||||
@ -587,7 +587,7 @@ func TestAzureMonitorCreateRequest(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
name: "creates a request",
|
||||
expectedURL: "http://ds/subscriptions",
|
||||
expectedURL: "http://ds/",
|
||||
expectedHeaders: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
},
|
||||
|
@ -18,6 +18,35 @@ type urlBuilder struct {
|
||||
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
|
||||
// should be returned
|
||||
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
|
||||
if resourceURI == "" {
|
||||
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{
|
||||
subscription,
|
||||
"resourceGroups",
|
||||
params.ResourceGroup,
|
||||
"providers",
|
||||
provider,
|
||||
}
|
||||
|
||||
for i := range metricDefinitionArray {
|
||||
urlArray = append(urlArray, metricDefinitionArray[i])
|
||||
urlArray = append(urlArray, resourceNameArray[i])
|
||||
}
|
||||
|
||||
resourceURI = strings.Join(urlArray[:], "/")
|
||||
resourceURI = params.buildMetricsURLFromLegacyQuery()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/providers/microsoft.insights/metrics", resourceURI)
|
||||
|
@ -3,23 +3,23 @@ package metrics
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestURLBuilder(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) {
|
||||
ub := &urlBuilder{
|
||||
ResourceURI: "resource/uri",
|
||||
ResourceURI: "/subscriptions/sub/resource/uri",
|
||||
}
|
||||
|
||||
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) {
|
||||
ub := &urlBuilder{
|
||||
ResourceURI: "resource/uri",
|
||||
ResourceURI: "/subscriptions/sub/resource/uri",
|
||||
DefaultSubscription: "default-sub",
|
||||
ResourceGroup: "rg",
|
||||
MetricDefinition: "Microsoft.NetApp/netAppAccounts/capacityPools/volumes",
|
||||
@ -27,7 +27,7 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -40,7 +40,7 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -53,7 +53,7 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -65,7 +65,7 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -77,7 +77,7 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -89,7 +89,7 @@ func TestURLBuilder(t *testing.T) {
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -121,9 +121,12 @@ type AzureMonitorJSONQuery struct {
|
||||
|
||||
// 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
|
||||
// Deprecated
|
||||
MetricDefinition string `json:"metricDefinition"`
|
||||
ResourceGroup string `json:"resourceGroup"`
|
||||
ResourceName string `json:"resourceName"`
|
||||
// Deprecated
|
||||
ResourceGroup string `json:"resourceGroup"`
|
||||
// Deprecated
|
||||
ResourceName string `json:"resourceName"`
|
||||
|
||||
AllowedTimeGrainsMs []int64 `json:"allowedTimeGrainsMs"`
|
||||
Dimension string `json:"dimension"` // old model
|
||||
|
@ -16,6 +16,14 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
|
||||
},
|
||||
getSubscriptions: jest.fn().mockResolvedValueOnce([]),
|
||||
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([]),
|
||||
@ -41,6 +49,7 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
|
||||
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]),
|
||||
getResourcesForResourceGroup: jest.fn().mockResolvedValue([]),
|
||||
getResourceURIFromWorkspace: jest.fn().mockReturnValue(''),
|
||||
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
|
@ -17,6 +17,8 @@ export default function createMockQuery(): AzureMonitorQuery {
|
||||
|
||||
azureMonitor: {
|
||||
// aggOptions: [],
|
||||
resourceUri:
|
||||
'/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572/resourceGroups/grafanastaging/providers/Microsoft.Compute/virtualMachines/grafana',
|
||||
aggregation: 'Average',
|
||||
allowedTimeGrainsMs: [60000, 300000, 900000, 1800000, 3600000, 21600000, 43200000, 86400000],
|
||||
// dimensionFilter: '*',
|
||||
|
@ -10,6 +10,7 @@ export default function createMockResourcePickerData(overrides?: DeepPartial<Res
|
||||
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]),
|
||||
getResourcesForResourceGroup: jest.fn().mockResolvedValue([]),
|
||||
getResourceURIFromWorkspace: jest.fn().mockReturnValue(''),
|
||||
getResourceURIDisplayProperties: jest.fn().mockResolvedValue({}),
|
||||
...overrides,
|
||||
};
|
||||
|
||||
|
@ -31,7 +31,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
|
||||
{
|
||||
id: 'dev-1',
|
||||
uri: '/subscriptions/def-456/resourceGroups/dev-1',
|
||||
name: 'Development',
|
||||
name: 'Development 1',
|
||||
type: ResourceRowType.ResourceGroup,
|
||||
typeLabel: 'Resource Group',
|
||||
children: [],
|
||||
@ -39,7 +39,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
|
||||
{
|
||||
id: 'dev-2',
|
||||
uri: '/subscriptions/def-456/resourceGroups/dev-2',
|
||||
name: 'Development',
|
||||
name: 'Development 2',
|
||||
type: ResourceRowType.ResourceGroup,
|
||||
typeLabel: 'Resource Group',
|
||||
children: [],
|
||||
@ -55,7 +55,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
|
||||
{
|
||||
id: 'dev-4',
|
||||
uri: '/subscriptions/def-456/resourceGroups/dev-4',
|
||||
name: 'Development',
|
||||
name: 'Development 3',
|
||||
type: ResourceRowType.ResourceGroup,
|
||||
typeLabel: 'Resource Group',
|
||||
children: [],
|
||||
@ -63,7 +63,7 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
|
||||
{
|
||||
id: 'dev-5',
|
||||
uri: '/subscriptions/def-456/resourceGroups/dev-5',
|
||||
name: 'Development',
|
||||
name: 'Development 4',
|
||||
type: ResourceRowType.ResourceGroup,
|
||||
typeLabel: 'Resource Group',
|
||||
children: [],
|
||||
|
@ -75,266 +75,59 @@ describe('AzureMonitorDatasource', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('When performing getSubscriptions', () => {
|
||||
|
||||
describe('When performing newGetMetricNamespaces', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
|
||||
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',
|
||||
id: '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1/providers/microsoft.insights/metricNamespaces/Azure.ApplicationInsights',
|
||||
name: 'Azure.ApplicationInsights',
|
||||
type: 'Microsoft.Insights/metricNamespaces',
|
||||
classification: 'Custom',
|
||||
properties: {
|
||||
metricNamespaceName: 'Azure.ApplicationInsights',
|
||||
},
|
||||
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',
|
||||
name: 'northeur',
|
||||
type: 'Microsoft.Compute/virtualMachines',
|
||||
},
|
||||
{
|
||||
location: 'westcentralus',
|
||||
name: 'us',
|
||||
type: 'Microsoft.Compute/virtualMachines',
|
||||
},
|
||||
{
|
||||
name: 'IHaveNoMetrics',
|
||||
type: 'IShouldBeFilteredOut',
|
||||
},
|
||||
{
|
||||
name: 'storageTest',
|
||||
type: 'Microsoft.Storage/storageAccounts',
|
||||
id: '/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1/providers/microsoft.insights/metricNamespaces/microsoft.insights-components',
|
||||
name: 'microsoft.insights-components',
|
||||
type: 'Microsoft.Insights/metricNamespaces',
|
||||
classification: 'Platform',
|
||||
properties: {
|
||||
metricNamespaceName: 'microsoft.insights/components',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
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');
|
||||
const basePath = 'azuremonitor/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp';
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return list of Metric Definitions with no duplicates and no unsupported namespaces', () => {
|
||||
return ctx.ds
|
||||
.getMetricDefinitions('9935389e-9122-4ef9-95f9-1513dd24753f', 'nodesapp')
|
||||
it('should return list of Metric Namspaces', () => {
|
||||
return ctx.ds.azureMonitorDatasource
|
||||
.newGetMetricNamespaces(
|
||||
'/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1'
|
||||
)
|
||||
.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');
|
||||
expect(results.length).toEqual(2);
|
||||
expect(results[0].text).toEqual('Azure.ApplicationInsights');
|
||||
expect(results[0].value).toEqual('Azure.ApplicationInsights');
|
||||
expect(results[1].text).toEqual('microsoft.insights-components');
|
||||
expect(results[1].value).toEqual('microsoft.insights/components');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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: [
|
||||
{
|
||||
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', () => {
|
||||
describe('When performing newGetMetricNames', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
@ -383,12 +176,9 @@ describe('AzureMonitorDatasource', () => {
|
||||
});
|
||||
|
||||
it('should return list of Metric Definitions', () => {
|
||||
return ctx.ds
|
||||
.getMetricNames(
|
||||
'9935389e-9122-4ef9-95f9-1513dd24753f',
|
||||
'nodeapp',
|
||||
'microsoft.insights/components',
|
||||
'resource1',
|
||||
return ctx.ds.azureMonitorDatasource
|
||||
.newGetMetricNames(
|
||||
'/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1',
|
||||
'default'
|
||||
)
|
||||
.then((results: Array<{ text: string; value: string }>) => {
|
||||
@ -401,7 +191,7 @@ describe('AzureMonitorDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getMetricMetadata', () => {
|
||||
describe('When performing newGetMetricMetadata', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
@ -450,12 +240,9 @@ describe('AzureMonitorDatasource', () => {
|
||||
});
|
||||
|
||||
it('should return Aggregation metadata for a Metric', () => {
|
||||
return ctx.ds
|
||||
.getMetricMetadata(
|
||||
'9935389e-9122-4ef9-95f9-1513dd24753f',
|
||||
'nodeapp',
|
||||
'microsoft.insights/components',
|
||||
'resource1',
|
||||
return ctx.ds.azureMonitorDatasource
|
||||
.newGetMetricMetadata(
|
||||
'/subscriptions/9935389e-9122-4ef9-95f9-1513dd24753f/resourceGroups/nodeapp/providers/microsoft.insights/components/resource1',
|
||||
'default',
|
||||
'UsedCapacity'
|
||||
)
|
||||
@ -467,130 +254,524 @@ describe('AzureMonitorDatasource', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('When performing getMetricMetadata on metrics with dimensions', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
name: {
|
||||
value: 'Transactions',
|
||||
localizedValue: 'Transactions',
|
||||
describe('Legacy Azure Monitor Query Object data fetchers', () => {
|
||||
describe('When performing getSubscriptions', () => {
|
||||
const response = {
|
||||
value: [
|
||||
{
|
||||
id: '/subscriptions/99999999-cccc-bbbb-aaaa-9106972f9572',
|
||||
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',
|
||||
supportedAggregationTypes: ['None', 'Average', 'Minimum', 'Maximum', 'Total', 'Count'],
|
||||
isDimensionRequired: false,
|
||||
dimensions: [
|
||||
],
|
||||
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',
|
||||
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',
|
||||
localizedValue: 'Response type',
|
||||
name: 'Failure Anomalies - nodeapp',
|
||||
type: 'microsoft.insights/alertrules',
|
||||
},
|
||||
{
|
||||
value: 'GeoType',
|
||||
localizedValue: 'Geo type',
|
||||
},
|
||||
{
|
||||
value: 'ApiName',
|
||||
localizedValue: 'API name',
|
||||
name: resourceGroup,
|
||||
type: metricDefinition,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: {
|
||||
value: 'FreeCapacity',
|
||||
localizedValue: 'Free capacity',
|
||||
};
|
||||
|
||||
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 = {
|
||||
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',
|
||||
supportedAggregationTypes: ['None', 'Average'],
|
||||
},
|
||||
],
|
||||
};
|
||||
{
|
||||
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 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",
|
||||
},
|
||||
]
|
||||
`);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
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 list of Metric Definitions', () => {
|
||||
return ctx.ds
|
||||
.getMetricNames(
|
||||
'9935389e-9122-4ef9-95f9-1513dd24753f',
|
||||
'nodeapp',
|
||||
'microsoft.insights/components',
|
||||
'resource1',
|
||||
'default'
|
||||
)
|
||||
.then((results: Array<{ text: string; value: string }>) => {
|
||||
expect(results.length).toEqual(2);
|
||||
expect(results[0].text).toEqual('Used capacity');
|
||||
expect(results[0].value).toEqual('UsedCapacity');
|
||||
expect(results[1].text).toEqual('Free capacity');
|
||||
expect(results[1].value).toEqual('FreeCapacity');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
describe('When performing getMetricMetadata', () => {
|
||||
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' },
|
||||
],
|
||||
},
|
||||
{
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -9,6 +9,9 @@ import TimegrainConverter from '../time_grain_converter';
|
||||
import {
|
||||
AzureDataSourceJsonData,
|
||||
AzureMonitorMetricDefinitionsResponse,
|
||||
AzureMonitorMetricNamespacesResponse,
|
||||
AzureMonitorMetricNamesResponse,
|
||||
AzureMonitorMetricsMetadataResponse,
|
||||
AzureMonitorQuery,
|
||||
AzureMonitorResourceGroupsResponse,
|
||||
AzureQueryType,
|
||||
@ -21,6 +24,10 @@ import UrlBuilder from './url_builder';
|
||||
|
||||
const defaultDropdownValue = 'select';
|
||||
|
||||
function hasValue(item?: string) {
|
||||
return !!(item && item !== defaultDropdownValue);
|
||||
}
|
||||
|
||||
export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureMonitorQuery, AzureDataSourceJsonData> {
|
||||
apiVersion = '2018-01-01';
|
||||
apiPreviewVersion = '2017-12-01-preview';
|
||||
@ -40,7 +47,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
this.defaultSubscriptionId = instanceSettings.jsonData.subscriptionId;
|
||||
|
||||
const cloud = getAzureCloud(instanceSettings);
|
||||
this.resourcePath = `${routeNames.azureMonitor}/subscriptions`;
|
||||
this.resourcePath = routeNames.azureMonitor;
|
||||
this.supportedMetricNamespaces = new SupportedNamespaces(cloud).get();
|
||||
this.azurePortalUrl = getAzurePortalUrl(cloud);
|
||||
}
|
||||
@ -51,19 +58,17 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
}
|
||||
|
||||
filterQuery(item: AzureMonitorQuery): boolean {
|
||||
const hasResourceUri = !!item?.azureMonitor?.resourceUri;
|
||||
const hasLegacyQuery =
|
||||
hasValue(item?.azureMonitor?.resourceGroup) &&
|
||||
hasValue(item?.azureMonitor?.resourceName) &&
|
||||
hasValue(item?.azureMonitor?.metricDefinition);
|
||||
|
||||
return !!(
|
||||
item.hide !== true &&
|
||||
item.azureMonitor &&
|
||||
item.azureMonitor.resourceGroup &&
|
||||
item.azureMonitor.resourceGroup !== defaultDropdownValue &&
|
||||
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
|
||||
(hasResourceUri || hasLegacyQuery) &&
|
||||
hasValue(item?.azureMonitor?.metricName) &&
|
||||
hasValue(item?.azureMonitor?.aggregation)
|
||||
);
|
||||
}
|
||||
|
||||
@ -82,6 +87,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
|
||||
const templateSrv = getTemplateSrv();
|
||||
|
||||
const resourceUri = templateSrv.replace(item.resourceUri, scopedVars);
|
||||
const subscriptionId = templateSrv.replace(target.subscription || this.defaultSubscriptionId, scopedVars);
|
||||
const resourceGroup = templateSrv.replace(item.resourceGroup, scopedVars);
|
||||
const resourceName = templateSrv.replace(item.resourceName, scopedVars);
|
||||
@ -107,6 +113,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
subscription: subscriptionId,
|
||||
queryType: AzureQueryType.AzureMonitor,
|
||||
azureMonitor: {
|
||||
resourceUri,
|
||||
resourceGroup,
|
||||
resourceName,
|
||||
metricDefinition,
|
||||
@ -128,14 +135,14 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
getResourceGroups(subscriptionId: string) {
|
||||
return this.getResource(
|
||||
`${this.resourcePath}/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
|
||||
`${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups?api-version=${this.listByResourceGroupApiVersion}`
|
||||
).then((result: AzureMonitorResourceGroupsResponse) => {
|
||||
return ResponseParser.parseResponseValues(result, 'name', 'name');
|
||||
});
|
||||
@ -143,7 +150,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
|
||||
getMetricDefinitions(subscriptionId: string, resourceGroup: string) {
|
||||
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) => {
|
||||
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) {
|
||||
let url =
|
||||
`${this.resourcePath}/${subscriptionId}/resourceGroups/${resourceGroup}/resources?` +
|
||||
`${this.resourcePath}/subscriptions/${subscriptionId}/resourceGroups/${resourceGroup}/resources?` +
|
||||
`$filter=resourceType eq '${metricDefinition}'&` +
|
||||
`api-version=${this.listByResourceGroupApiVersion}`;
|
||||
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(
|
||||
subscriptionId: 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(
|
||||
subscriptionId: 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> {
|
||||
const validationError = this.validateDatasource();
|
||||
if (validationError) {
|
||||
@ -296,7 +344,7 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -1,180 +1,209 @@
|
||||
import UrlBuilder from './url_builder';
|
||||
|
||||
describe('AzureMonitorUrlBuilder', () => {
|
||||
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => {
|
||||
it('should build the getMetricNamespaces url in the even longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
||||
describe('when a resource uri is provided', () => {
|
||||
it('builds a getMetricNamesnamespace url', () => {
|
||||
const url = UrlBuilder.newBuildAzureMonitorGetMetricNamespacesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
|
||||
'rn1/rn2/rn3',
|
||||
'/subscriptions/sub/resource-uri/resource',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
|
||||
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
|
||||
'/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Sql/servers/databases', () => {
|
||||
it('should build the getMetricNamespaces url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
||||
describe('when a resource uri and metric namespace is provided', () => {
|
||||
it('builds a getMetricNames url', () => {
|
||||
const url = UrlBuilder.newBuildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'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',
|
||||
'/subscriptions/sub/resource-uri/resource',
|
||||
'Microsoft.Sql/servers',
|
||||
'rn',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
|
||||
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
|
||||
'/subscriptions/sub/resource-uri/resource/providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=Microsoft.Sql%2Fservers'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => {
|
||||
it('should build the getMetricNames url in the even longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
|
||||
'rn1/rn2/rn3',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
|
||||
);
|
||||
describe('Legacy query object', () => {
|
||||
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes', () => {
|
||||
it('should build the getMetricNamespaces url in the even longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
|
||||
'rn1/rn2/rn3',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.NetApp/netAppAccounts/rn1/capacityPools/rn2/volumes/rn3/' +
|
||||
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Sql/servers/databases and the metricNamespace is default', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'rn1/rn2',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
|
||||
);
|
||||
describe('when metric definition is Microsoft.Sql/servers/databases', () => {
|
||||
it('should build the getMetricNamespaces url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamespacesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'rn1/rn2',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/subscriptions/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 and the metricNamespace is default', () => {
|
||||
it('should build the getMetricNames url in the shorter format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.Sql/servers',
|
||||
'rn',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
|
||||
);
|
||||
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',
|
||||
'rn',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
|
||||
'providers/microsoft.insights/metricNamespaces?api-version=2017-05-01-preview'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when metric definition is Microsoft.Storage/storageAccounts/blobServices and the metricNamespace is default', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'rn1/default',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/default/' +
|
||||
'providers/microsoft.insights/metricdefinitions?api-version=2017-05-01-preview&metricnamespace=default'
|
||||
);
|
||||
describe('when metric definition is Microsoft.NetApp/netAppAccounts/capacityPools/volumes and the metricNamespace is default', () => {
|
||||
it('should build the getMetricNames url in the even longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.NetApp/netAppAccounts/capacityPools/volumes',
|
||||
'rn1/rn2/rn3',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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(
|
||||
'/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.Sql/servers/databases and the metricNamespace is default', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.Sql/servers/databases',
|
||||
'rn1/rn2',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn1/databases/rn2/' +
|
||||
'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(
|
||||
'/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.Sql/servers and the metricNamespace is default', () => {
|
||||
it('should build the getMetricNames url in the shorter format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.Sql/servers',
|
||||
'rn',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Sql/servers/rn/' +
|
||||
'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(
|
||||
'/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/queueServices/default/' +
|
||||
'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', () => {
|
||||
it('should build the getMetricNames url in the longer format', () => {
|
||||
const url = UrlBuilder.buildAzureMonitorGetMetricNamesUrl(
|
||||
'',
|
||||
'sub1',
|
||||
'rg',
|
||||
'Microsoft.Storage/storageAccounts/blobServices',
|
||||
'rn1/default',
|
||||
'default',
|
||||
'2017-05-01-preview'
|
||||
);
|
||||
expect(url).toBe(
|
||||
'/subscriptions/sub1/resourceGroups/rg/providers/Microsoft.Storage/storageAccounts/rn1/blobServices/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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -10,7 +10,7 @@ export default class UrlBuilder {
|
||||
const metricDefinitionArray = metricDefinition.split('/');
|
||||
const resourceNameArray = resourceName.split('/');
|
||||
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) {
|
||||
urlArray.push(metricDefinitionArray[i]);
|
||||
urlArray.push(resourceNameArray[i]);
|
||||
@ -31,7 +31,7 @@ export default class UrlBuilder {
|
||||
const metricDefinitionArray = metricDefinition.split('/');
|
||||
const resourceNameArray = resourceName.split('/');
|
||||
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) {
|
||||
urlArray.push(metricDefinitionArray[i]);
|
||||
urlArray.push(resourceNameArray[i]);
|
||||
@ -42,4 +42,20 @@ export default class UrlBuilder {
|
||||
`&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)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ import React from 'react';
|
||||
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
|
||||
import Datasource from '../../datasource';
|
||||
import { Alert, InlineFieldRow } from '@grafana/ui';
|
||||
import { ResourceRowType } from '../ResourcePicker/types';
|
||||
import ResourceField from '../ResourceField';
|
||||
import QueryField from './QueryField';
|
||||
import FormatAsField from './FormatAsField';
|
||||
import ResourceField from './ResourceField';
|
||||
import useMigrations from './useMigrations';
|
||||
import { setResource } from './setQueryValue';
|
||||
|
||||
interface LogsQueryEditorProps {
|
||||
query: AzureMonitorQuery;
|
||||
@ -38,6 +40,14 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
setError={setError}
|
||||
selectableEntryTypes={[
|
||||
ResourceRowType.Subscription,
|
||||
ResourceRowType.ResourceGroup,
|
||||
ResourceRowType.Resource,
|
||||
ResourceRowType.Variable,
|
||||
]}
|
||||
setResource={setResource}
|
||||
resourceUri={query.azureLogAnalytics?.resource}
|
||||
/>
|
||||
</InlineFieldRow>
|
||||
|
||||
|
@ -6,7 +6,6 @@ import { AzureMetricQuery, AzureMonitorOption, AzureMonitorQuery, AzureQueryType
|
||||
import {
|
||||
DataHook,
|
||||
updateSubscriptions,
|
||||
useAsyncState,
|
||||
useMetricNames,
|
||||
useMetricNamespaces,
|
||||
useResourceGroups,
|
||||
@ -15,75 +14,12 @@ import {
|
||||
useSubscriptions,
|
||||
} from './dataHooks';
|
||||
|
||||
interface WaitableMock extends jest.Mock<any, any> {
|
||||
waitToBeCalled(): Promise<unknown>;
|
||||
}
|
||||
|
||||
const WAIT_OPTIONS = {
|
||||
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 });
|
||||
|
||||
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 {
|
||||
name: string;
|
||||
hook: DataHook;
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Datasource from '../../datasource';
|
||||
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
|
||||
import { hasOption, toOption } from '../../utils/common';
|
||||
import { useAsyncState } from '../../utils/useAsyncState';
|
||||
import { setMetricNamespace, setSubscriptionID } from './setQueryValue';
|
||||
|
||||
export interface MetricMetadata {
|
||||
@ -26,29 +27,6 @@ export type DataHook = (
|
||||
setError: SetErrorFn
|
||||
) => 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 = (
|
||||
query: AzureMonitorQuery,
|
||||
subscriptionOptions: AzureMonitorOption[],
|
||||
|
@ -1,5 +1,20 @@
|
||||
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 {
|
||||
if (query.subscription === subscriptionID) {
|
||||
return query;
|
||||
|
@ -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',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
@ -1,9 +1,113 @@
|
||||
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> = ({}) => {
|
||||
return <div data-testid="azure-monitor-metrics-query-editor-with-resource-picker">New Query Editor</div>;
|
||||
const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
|
||||
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;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
@ -99,7 +99,15 @@ const EditorForQueryType: React.FC<EditorForQueryTypeProps> = ({
|
||||
switch (query.queryType) {
|
||||
case AzureQueryType.AzureMonitor:
|
||||
if (config.featureToggles.azureMonitorResourcePickerForMetrics) {
|
||||
return <NewMetricsQueryEditor />;
|
||||
return (
|
||||
<NewMetricsQueryEditor
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
setError={setError}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<MetricsQueryEditor
|
||||
|
@ -4,13 +4,12 @@ import { Button, Icon, Modal, useStyles2 } from '@grafana/ui';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import Datasource from '../../datasource';
|
||||
import { AzureQueryEditorFieldProps, AzureResourceSummaryItem } from '../../types';
|
||||
import { AzureQueryEditorFieldProps, AzureMonitorQuery, AzureResourceSummaryItem } from '../../types';
|
||||
import { Field } from '../Field';
|
||||
import ResourcePicker from '../ResourcePicker';
|
||||
import { ResourceRowType } from '../ResourcePicker/types';
|
||||
import { parseResourceURI } from '../ResourcePicker/utils';
|
||||
import { Space } from '../Space';
|
||||
import { setResource } from './setQueryValue';
|
||||
|
||||
function parseResourceDetails(resourceURI: string) {
|
||||
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 { resource } = query.azureLogAnalytics ?? {};
|
||||
const [pickerIsOpen, setPickerIsOpen] = useState(false);
|
||||
|
||||
const handleOpenPicker = useCallback(() => {
|
||||
@ -44,7 +55,7 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
|
||||
onQueryChange(setResource(query, resourceURI));
|
||||
closePicker();
|
||||
},
|
||||
[closePicker, onQueryChange, query]
|
||||
[closePicker, onQueryChange, query, setResource]
|
||||
);
|
||||
|
||||
return (
|
||||
@ -60,21 +71,16 @@ const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource
|
||||
>
|
||||
<ResourcePicker
|
||||
resourcePickerData={datasource.resourcePickerData}
|
||||
resourceURI={resource}
|
||||
resourceURI={resourceUri}
|
||||
onApply={handleApply}
|
||||
onCancel={closePicker}
|
||||
selectableEntryTypes={[
|
||||
ResourceRowType.Subscription,
|
||||
ResourceRowType.ResourceGroup,
|
||||
ResourceRowType.Resource,
|
||||
ResourceRowType.Variable,
|
||||
]}
|
||||
selectableEntryTypes={selectableEntryTypes}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Field label="Resource">
|
||||
<Button variant="secondary" onClick={handleOpenPicker} type="button">
|
||||
<ResourceLabel resource={resource} datasource={datasource} />
|
||||
<ResourceLabel resource={resourceUri} datasource={datasource} />
|
||||
</Button>
|
||||
</Field>
|
||||
</>
|
@ -0,0 +1 @@
|
||||
export { default } from './ResourceField';
|
@ -36,6 +36,7 @@ export interface AzureMonitorQuery extends DeprecatedAzureMonitorQuery {
|
||||
* Azure Monitor Metrics sub-query properties
|
||||
*/
|
||||
export interface AzureMetricQuery {
|
||||
resourceUri?: string;
|
||||
resourceGroup?: string;
|
||||
|
||||
/** Resource type */
|
||||
|
@ -95,6 +95,23 @@ export interface AzureMonitorMetricMetadataItem {
|
||||
metricAvailabilities?: AzureMonitorMetricAvailabilityMetadata[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNamespacesResponse {
|
||||
value: AzureMonitorMetricNamespaceItem[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNamespaceItem {
|
||||
name: string;
|
||||
properties: { metricNamespacename: string };
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNamesResponse {
|
||||
value: AzureMonitorMetricNameItem[];
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricNameItem {
|
||||
name: { value: string; localizedValue: string };
|
||||
}
|
||||
|
||||
export interface AzureMonitorMetricAvailabilityMetadata {
|
||||
timeGrain: string;
|
||||
retention: string;
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user