mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
dae9808602
commit
bed1bb1a73
@ -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}');
|
||||
|
@ -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 }]])
|
||||
),
|
||||
},
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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',
|
||||
})
|
||||
);
|
||||
|
@ -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"
|
||||
|
@ -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) {
|
||||
|
@ -95,6 +95,9 @@ export const components = {
|
||||
resource: {
|
||||
input: 'data-testid resource',
|
||||
},
|
||||
region: {
|
||||
input: 'data-testid region',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -263,6 +263,7 @@ export interface AzureGetResourceNamesQuery {
|
||||
subscriptionId: string;
|
||||
resourceGroup?: string;
|
||||
metricNamespace?: string;
|
||||
region?: string;
|
||||
}
|
||||
|
||||
export interface AzureMonitorLocations {
|
||||
|
@ -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)] : [],
|
||||
|
Loading…
Reference in New Issue
Block a user