Azure Monitor: Allow to specify a region when listing resources (#62306)

* Azure Monitor: Allow to specify a region when listing resources

* Add region template variable to e2e tests
This commit is contained in:
Andres Martinez Gotor 2023-01-30 10:49:20 +01:00 committed by GitHub
parent dae9808602
commit bed1bb1a73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 126 additions and 47 deletions

View File

@ -56,7 +56,7 @@ const addAzureMonitorVariable = (
name: string,
type: AzureQueryType,
isFirst: boolean,
options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string }
options?: { subscription?: string; resourceGroup?: string; namespace?: string; resource?: string; region?: string }
) => {
e2e.components.PageToolbar.item('Dashboard settings').click();
e2e.components.Tab.title('Variables').click();
@ -75,6 +75,9 @@ const addAzureMonitorVariable = (
case AzureQueryType.ResourceGroupsQuery:
e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`);
break;
case AzureQueryType.LocationsQuery:
e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`);
break;
case AzureQueryType.NamespacesQuery:
e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`);
e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`);
@ -83,6 +86,7 @@ const addAzureMonitorVariable = (
e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`);
e2eSelectors.variableEditor.resourceGroup.input().find('input').type(`${options?.resourceGroup}{enter}`);
e2eSelectors.variableEditor.namespace.input().find('input').type(`${options?.namespace}{enter}`);
e2eSelectors.variableEditor.region.input().find('input').type(`${options?.region}{enter}`);
break;
case AzureQueryType.MetricNamesQuery:
e2eSelectors.variableEditor.subscription.input().find('input').type(`${options?.subscription}{enter}`);
@ -223,10 +227,14 @@ e2e.scenario({
subscription: '$subscription',
resourceGroup: '$resourceGroups',
});
addAzureMonitorVariable('region', AzureQueryType.LocationsQuery, false, {
subscription: '$subscription',
});
addAzureMonitorVariable('resource', AzureQueryType.ResourceNamesQuery, false, {
subscription: '$subscription',
resourceGroup: '$resourceGroups',
namespace: '$namespace',
region: '$region',
});
e2e.pages.Dashboard.SubMenu.submenuItemLabels('subscription').click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('grafanalabs-datasources-dev').click();
@ -254,6 +262,8 @@ e2e.scenario({
e2eSelectors.queryEditor.resourcePicker.advanced.subscription.input().find('input').type('$subscription');
e2eSelectors.queryEditor.resourcePicker.advanced.resourceGroup.input().find('input').type('$resourceGroups');
e2eSelectors.queryEditor.resourcePicker.advanced.namespace.input().find('input').type('$namespaces');
// TODO: Enable this input once multiple resources feature flag is removed
// e2eSelectors.queryEditor.resourcePicker.advanced.region.input().find('input').type('$region');
e2eSelectors.queryEditor.resourcePicker.advanced.resource.input().find('input').type('$resource');
e2eSelectors.queryEditor.resourcePicker.apply.button().click();
e2eSelectors.queryEditor.metricsQueryEditor.metricName.input().find('input').type('Transactions{enter}');

View File

@ -32,7 +32,7 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
}),
getLocations: jest
.fn()
.mockResolvedValueOnce(
.mockResolvedValue(
new Map([['northeurope', { displayName: 'North Europe', name: 'northeurope', supportsLogs: false }]])
),
},

View File

@ -580,6 +580,7 @@ describe('AzureMonitorDatasource', () => {
let subscription = 'mock-subscription-id';
let resourceGroup = 'nodeapp';
let metricNamespace = 'microsoft.insights/components';
let region = '';
beforeEach(() => {
subscription = 'mock-subscription-id';
@ -605,7 +606,9 @@ describe('AzureMonitorDatasource', () => {
ctx.ds.azureMonitorDatasource.getResource = jest.fn().mockImplementation((path: string) => {
const basePath = `azuremonitor/subscriptions/${subscription}/resourceGroups`;
expect(path).toBe(
`${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'`
`${basePath}/${resourceGroup}/resources?api-version=2021-04-01&$filter=resourceType eq '${metricNamespace}'${
region ? ` and location eq '${region}'` : ''
}`
);
return Promise.resolve(response);
});
@ -632,11 +635,22 @@ describe('AzureMonitorDatasource', () => {
});
});
it('should return include a region', () => {
region = 'eastus';
return ctx.ds
.getResourceNames(subscription, resourceGroup, metricNamespace, region)
.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 multiple resources from a template variable', () => {
const tsrv = new TemplateSrv();
tsrv.replace = jest
.fn()
.mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value));
.mockImplementation((value: string) => (value === `$${multiVariable.id}` ? 'foo,bar' : value ?? ''));
const ds = new AzureMonitorDatasource(ctx.instanceSettings, templateSrv);
ds.azureMonitorDatasource.templateSrv = tsrv;
ds.azureMonitorDatasource.getResource = jest

View File

@ -165,47 +165,56 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
}
async getResourceNames(query: AzureGetResourceNamesQuery, skipToken?: string) {
const promises = this.replaceTemplateVariables(query).map(({ metricNamespace, subscriptionId, resourceGroup }) => {
const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')
? 'microsoft.storage/storageaccounts'
: metricNamespace;
let url = `${this.resourcePath}/subscriptions/${subscriptionId}`;
if (resourceGroup) {
url += `/resourceGroups/${resourceGroup}`;
}
url += `/resources?api-version=${this.listByResourceGroupApiVersion}`;
if (validMetricNamespace) {
url += `&$filter=resourceType eq '${validMetricNamespace}'`;
}
if (skipToken) {
url += `&$skiptoken=${skipToken}`;
}
return this.getResource(url).then(async (result: any) => {
let list: Array<{ text: string; value: string }> = [];
if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) {
list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts');
for (let i = 0; i < list.length; i++) {
list[i].text += '/default';
list[i].value += '/default';
}
} else {
list = ResponseParser.parseResourceNames(result, metricNamespace);
const promises = this.replaceTemplateVariables(query).map(
({ metricNamespace, subscriptionId, resourceGroup, region }) => {
const validMetricNamespace = startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')
? 'microsoft.storage/storageaccounts'
: metricNamespace;
let url = `${this.resourcePath}/subscriptions/${subscriptionId}`;
if (resourceGroup) {
url += `/resourceGroups/${resourceGroup}`;
}
if (result.nextLink) {
// If there is a nextLink, we should request more pages
const nextURL = new URL(result.nextLink);
const nextToken = nextURL.searchParams.get('$skiptoken');
if (!nextToken) {
throw Error('unable to request the next page of resources');
}
const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken);
list = list.concat(nextPage);
url += `/resources?api-version=${this.listByResourceGroupApiVersion}`;
const filters: string[] = [];
if (validMetricNamespace) {
filters.push(`resourceType eq '${validMetricNamespace}'`);
}
if (region) {
filters.push(`location eq '${region}'`);
}
if (filters.length > 0) {
url += `&$filter=${filters.join(' and ')}`;
}
if (skipToken) {
url += `&$skiptoken=${skipToken}`;
}
return this.getResource(url).then(async (result: any) => {
let list: Array<{ text: string; value: string }> = [];
if (startsWith(metricNamespace?.toLowerCase(), 'microsoft.storage/storageaccounts/')) {
list = ResponseParser.parseResourceNames(result, 'microsoft.storage/storageaccounts');
for (let i = 0; i < list.length; i++) {
list[i].text += '/default';
list[i].value += '/default';
}
} else {
list = ResponseParser.parseResourceNames(result, metricNamespace);
}
return list;
});
});
if (result.nextLink) {
// If there is a nextLink, we should request more pages
const nextURL = new URL(result.nextLink);
const nextToken = nextURL.searchParams.get('$skiptoken');
if (!nextToken) {
throw Error('unable to request the next page of resources');
}
const nextPage = await this.getResourceNames({ metricNamespace, subscriptionId, resourceGroup }, nextToken);
list = list.concat(nextPage);
}
return list;
});
}
);
return (await Promise.all(promises)).flat();
}
@ -347,7 +356,9 @@ export default class AzureMonitorDatasource extends DataSourceWithBackend<AzureM
for (const subscription of subscriptions) {
const subLocations = ResponseParser.parseLocations(
await this.getResource<AzureMonitorLocationsResponse>(
`${routeNames.azureMonitor}/subscriptions/${subscription}/locations?api-version=${this.locationsApiVersion}`
`${routeNames.azureMonitor}/subscriptions/${this.templateSrv.replace(subscription)}/locations?api-version=${
this.locationsApiVersion
}`
)
);
for (const location of subLocations) {

View File

@ -258,10 +258,12 @@ describe('VariableEditor:', () => {
await waitFor(() => expect(screen.getByText('Logs')).toBeInTheDocument());
await selectAndRerender('select query type', 'Resource Names', onChange, rerender);
await selectAndRerender('select subscription', 'Primary Subscription', onChange, rerender);
await selectAndRerender('select region', 'North Europe', onChange, rerender);
expect(onChange).toHaveBeenCalledWith(
expect.objectContaining({
queryType: AzureQueryType.ResourceNamesQuery,
subscription: 'sub',
region: 'northeurope',
refId: 'A',
})
);

View File

@ -51,6 +51,7 @@ const VariableEditor = (props: Props) => {
const [requireSubscription, setRequireSubscription] = useState(false);
const [hasResourceGroup, setHasResourceGroup] = useState(false);
const [hasNamespace, setHasNamespace] = useState(false);
const [hasRegion, setHasRegion] = useState(false);
const [requireResourceGroup, setRequireResourceGroup] = useState(false);
const [requireNamespace, setRequireNamespace] = useState(false);
const [requireResource, setRequireResource] = useState(false);
@ -58,6 +59,7 @@ const VariableEditor = (props: Props) => {
const [resourceGroups, setResourceGroups] = useState<SelectableValue[]>([]);
const [namespaces, setNamespaces] = useState<SelectableValue[]>([]);
const [resources, setResources] = useState<SelectableValue[]>([]);
const [regions, setRegions] = useState<SelectableValue[]>([]);
const [errorMessage, setError] = useLastError();
const queryType = typeof query === 'string' ? '' : query.queryType;
@ -87,6 +89,7 @@ const VariableEditor = (props: Props) => {
setRequireSubscription(true);
setHasResourceGroup(true);
setHasNamespace(true);
setHasRegion(true);
break;
case AzureQueryType.MetricNamesQuery:
setRequireSubscription(true);
@ -137,6 +140,16 @@ const VariableEditor = (props: Props) => {
}
}, [datasource, subscription, resourceGroup]);
useEffect(() => {
if (subscription) {
datasource.azureMonitorDatasource.getLocations([subscription]).then((rgs) => {
const regions: SelectableValue[] = [];
rgs.forEach((r) => regions.push({ label: r.displayName, value: r.name }));
setRegions(regions);
});
}
}, [datasource, subscription, resourceGroup]);
const namespace = (typeof query === 'object' && query.namespace) || '';
useEffect(() => {
if (subscription) {
@ -193,6 +206,13 @@ const VariableEditor = (props: Props) => {
});
};
const onChangeRegion = (selectableValue: SelectableValue) => {
onChange({
...query,
region: selectableValue.value,
});
};
const onChangeResource = (selectableValue: SelectableValue) => {
onChange({
...query,
@ -298,6 +318,22 @@ const VariableEditor = (props: Props) => {
/>
</InlineField>
)}
{hasRegion && (
<InlineField
label="Select region"
labelWidth={20}
data-testid={selectors.components.variableEditor.region.input}
>
<Select
aria-label="select region"
onChange={onChangeRegion}
options={regions.concat(variableOptionGroup)}
width={25}
value={query.region || null}
placeholder="Optional"
/>
</InlineField>
)}
{requireResource && (
<InlineField
label="Select resource"

View File

@ -155,8 +155,8 @@ export default class Datasource extends DataSourceWithBackend<AzureMonitorQuery,
return this.azureMonitorDatasource.getMetricNamespaces({ resourceUri: url }, true);
}
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string) {
return this.azureMonitorDatasource.getResourceNames({ subscriptionId, resourceGroup, metricNamespace });
getResourceNames(subscriptionId: string, resourceGroup?: string, metricNamespace?: string, region?: string) {
return this.azureMonitorDatasource.getResourceNames({ subscriptionId, resourceGroup, metricNamespace, region });
}
getMetricNames(subscriptionId: string, resourceGroup: string, metricNamespace: string, resourceName: string) {

View File

@ -95,6 +95,9 @@ export const components = {
resource: {
input: 'data-testid resource',
},
region: {
input: 'data-testid region',
},
},
};

View File

@ -12,7 +12,7 @@ export enum AzureQueryType {
ResourceNamesQuery = 'Azure Resource Names',
MetricNamesQuery = 'Azure Metric Names',
WorkspacesQuery = 'Azure Workspaces',
LocationsQuery = 'Azure Locations',
LocationsQuery = 'Azure Regions',
/** Deprecated */
GrafanaTemplateVariableFn = 'Grafana Template Variable Function',
}
@ -38,6 +38,7 @@ export interface AzureMonitorQuery extends DataQuery {
resourceGroup?: string;
namespace?: string;
resource?: string;
region?: string;
}
export interface AzureMonitorResource {

View File

@ -263,6 +263,7 @@ export interface AzureGetResourceNamesQuery {
subscriptionId: string;
resourceGroup?: string;
metricNamespace?: string;
region?: string;
}
export interface AzureMonitorLocations {

View File

@ -64,7 +64,8 @@ export class VariableSupport extends CustomVariableSupport<DataSource, AzureMoni
const rgs = await this.datasource.getResourceNames(
queryObj.subscription,
queryObj.resourceGroup,
queryObj.namespace
queryObj.namespace,
queryObj.region
);
return {
data: rgs?.length ? [toDataFrame(rgs)] : [],