Azure: Support scope selection in Resource Graph queries (#105835)
* Add scope type * Add scope selector field * Update docs * Use the right field component * Trigger build * Fix import and increase wait * i8n
This commit is contained in:
@@ -253,9 +253,14 @@ By querying ARG, you can query resources with complex filtering, iteratively exp
|
||||
### Create a Resource Graph query
|
||||
|
||||
ARG queries are written in a variant of the [Kusto Query Language (KQL)](https://docs.microsoft.com/en-us/azure/governance/resource-graph/concepts/query-language), but not all Kusto language features are available in ARG.
|
||||
|
||||
An Azure Resource Graph query is formatted as table data.
|
||||
|
||||
If your Azure credentials grant you access to multiple subscriptions, you can choose multiple subscriptions before entering queries.
|
||||
If your Azure credentials grant you access to multiple subscriptions, you can choose multiple subscriptions before entering queries. It is also possible to run queries against the directory by changing the scope of the query.
|
||||
|
||||
{{% admonition type="note" %}}
|
||||
Some queries that function at a directory level may not work at a subscription level and vice-versa.
|
||||
{{% /admonition %}}
|
||||
|
||||
### Resource Graph query examples
|
||||
|
||||
|
||||
@@ -221,7 +221,7 @@ describe('Azure monitor datasource', () => {
|
||||
visitDashboardAtStart: false,
|
||||
queriesForm: () => {
|
||||
e2eSelectors.queryEditor.header.select().find('input').type('Azure Resource Graph{enter}');
|
||||
cy.wait(1000); // Need to wait for code editor to completely load
|
||||
cy.wait(2000); // Need to wait for code editor to completely load
|
||||
e2eSelectors.queryEditor.argsQueryEditor.subscriptions.input().find('[aria-label="Clear value"]').click();
|
||||
e2eSelectors.queryEditor.argsQueryEditor.subscriptions.input().find('input').type('datasources{enter}');
|
||||
e2e.components.CodeEditor.container().type(
|
||||
|
||||
+9
@@ -452,6 +452,11 @@ export interface BuilderQueryExpression {
|
||||
where?: BuilderQueryEditorWhereExpressionArray;
|
||||
}
|
||||
|
||||
export enum ARGScope {
|
||||
Directory = 'directory',
|
||||
Subscription = 'subscription',
|
||||
}
|
||||
|
||||
export interface AzureResourceGraphQuery {
|
||||
/**
|
||||
* Azure Resource Graph KQL query to be executed.
|
||||
@@ -461,6 +466,10 @@ export interface AzureResourceGraphQuery {
|
||||
* Specifies the format results should be returned as. Defaults to table.
|
||||
*/
|
||||
resultFormat?: string;
|
||||
/**
|
||||
* Specifies the scope of the query. Defaults to subscription.
|
||||
*/
|
||||
scope?: ARGScope;
|
||||
}
|
||||
|
||||
export interface AzureMonitorResource {
|
||||
|
||||
@@ -407,6 +407,8 @@ type AzureResourceGraphQuery struct {
|
||||
Query *string `json:"query,omitempty"`
|
||||
// Specifies the format results should be returned as. Defaults to table.
|
||||
ResultFormat *string `json:"resultFormat,omitempty"`
|
||||
// Specifies the scope of the query. Defaults to subscription.
|
||||
Scope *ARGScope `json:"scope,omitempty"`
|
||||
}
|
||||
|
||||
// NewAzureResourceGraphQuery creates a new AzureResourceGraphQuery object.
|
||||
@@ -414,6 +416,13 @@ func NewAzureResourceGraphQuery() *AzureResourceGraphQuery {
|
||||
return &AzureResourceGraphQuery{}
|
||||
}
|
||||
|
||||
type ARGScope string
|
||||
|
||||
const (
|
||||
ARGScopeSubscription ARGScope = "subscription"
|
||||
ARGScopeDirectory ARGScope = "directory"
|
||||
)
|
||||
|
||||
// Application Insights Traces sub-query properties
|
||||
type AzureTracesQuery struct {
|
||||
// Specifies the format results should be returned as.
|
||||
|
||||
+14
-9
@@ -1,19 +1,20 @@
|
||||
import { startsWith, includes, find, filter } from 'lodash';
|
||||
import { filter, find, includes, startsWith } from 'lodash';
|
||||
|
||||
import { ScopedVars } from '@grafana/data';
|
||||
import { getTemplateSrv, DataSourceWithBackend, TemplateSrv } from '@grafana/runtime';
|
||||
import { DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime';
|
||||
|
||||
import { resourceTypes } from '../azureMetadata';
|
||||
import { ARGScope } from '../dataquery.gen';
|
||||
import {
|
||||
AzureMonitorQuery,
|
||||
AzureMonitorDataSourceJsonData,
|
||||
AzureQueryType,
|
||||
RawAzureResourceGroupItem,
|
||||
AzureGetResourceNamesQuery,
|
||||
AzureMonitorDataSourceInstanceSettings,
|
||||
RawAzureResourceItem,
|
||||
AzureGraphResponse,
|
||||
AzureMonitorDataSourceInstanceSettings,
|
||||
AzureMonitorDataSourceJsonData,
|
||||
AzureMonitorQuery,
|
||||
AzureQueryType,
|
||||
AzureResourceGraphOptions,
|
||||
RawAzureResourceGroupItem,
|
||||
RawAzureResourceItem,
|
||||
RawAzureSubscriptionItem,
|
||||
} from '../types';
|
||||
import { interpolateVariable, replaceTemplateVariables, routeNames } from '../utils/common';
|
||||
@@ -33,7 +34,10 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
|
||||
filterQuery(item: AzureMonitorQuery): boolean {
|
||||
return !!item.azureResourceGraph?.query && !!item.subscriptions && item.subscriptions.length > 0;
|
||||
return (
|
||||
!!item.azureResourceGraph?.query &&
|
||||
(item.azureResourceGraph.scope === ARGScope.Directory || (!!item.subscriptions && item.subscriptions.length > 0))
|
||||
);
|
||||
}
|
||||
|
||||
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): AzureMonitorQuery {
|
||||
@@ -61,6 +65,7 @@ export default class AzureResourceGraphDatasource extends DataSourceWithBackend<
|
||||
azureResourceGraph: {
|
||||
resultFormat: 'table',
|
||||
query,
|
||||
scope: item.scope,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+41
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import createMockDatasource from '../../__mocks__/datasource';
|
||||
import createMockQuery from '../../__mocks__/query';
|
||||
import { ARGScope } from '../../dataquery.gen';
|
||||
import { selectors } from '../../e2e/selectors';
|
||||
|
||||
import ArgQueryEditor from './ArgQueryEditor';
|
||||
@@ -30,6 +31,20 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
describe('ArgQueryEditor', () => {
|
||||
beforeAll(() => {
|
||||
const mockGetBoundingClientRect = jest.fn(() => ({
|
||||
width: 120,
|
||||
height: 120,
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
}));
|
||||
|
||||
Object.defineProperty(Element.prototype, 'getBoundingClientRect', {
|
||||
value: mockGetBoundingClientRect,
|
||||
});
|
||||
});
|
||||
it('should render', async () => {
|
||||
render(<ArgQueryEditor {...defaultProps} />);
|
||||
expect(
|
||||
@@ -37,6 +52,32 @@ describe('ArgQueryEditor', () => {
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should change the scope to directory', async () => {
|
||||
const datasource = createMockDatasource({
|
||||
getSubscriptions: jest.fn().mockResolvedValue([{ value: 'foo' }]),
|
||||
});
|
||||
const onChange = jest.fn();
|
||||
render(<ArgQueryEditor {...defaultProps} datasource={datasource} onChange={onChange} />);
|
||||
expect(await screen.findByTestId(selectors.components.queryEditor.argsQueryEditor.scope.input)).toBeInTheDocument();
|
||||
|
||||
const scopeSelector = screen.getByTestId(selectors.components.queryEditor.argsQueryEditor.scope.input);
|
||||
|
||||
await userEvent.click(scopeSelector);
|
||||
const directoryOption = await screen.findByRole('option', { name: 'Directory' });
|
||||
await userEvent.click(directoryOption);
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
azureResourceGraph: {
|
||||
query: 'Resources | summarize count()',
|
||||
resultFormat: 'table',
|
||||
scope: ARGScope.Directory,
|
||||
},
|
||||
subscriptions: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should select a subscription from the fetched array', async () => {
|
||||
const datasource = createMockDatasource({
|
||||
getSubscriptions: jest.fn().mockResolvedValue([{ value: 'foo' }]),
|
||||
|
||||
+61
-29
@@ -1,11 +1,16 @@
|
||||
import { intersection } from 'lodash';
|
||||
import { useState, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/plugin-ui';
|
||||
import { Combobox } from '@grafana/ui';
|
||||
|
||||
import { ARGScope } from '../../dataquery.gen';
|
||||
import Datasource from '../../datasource';
|
||||
import { selectors } from '../../e2e/selectors';
|
||||
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
|
||||
import { Field } from '../shared/Field';
|
||||
|
||||
import QueryField from './QueryField';
|
||||
import SubscriptionField from './SubscriptionField';
|
||||
@@ -54,44 +59,71 @@ const ArgQueryEditor = ({
|
||||
}: ArgQueryEditorProps) => {
|
||||
const [subscriptions, setSubscriptions] = useState<AzureMonitorOption[]>([]);
|
||||
useMemo(() => {
|
||||
datasource
|
||||
.getSubscriptions()
|
||||
.then((results) => {
|
||||
const selectAllSubscriptionOption = [
|
||||
{ label: 'Select all subscriptions', value: 'Select all subscriptions', description: 'Select all' },
|
||||
];
|
||||
const fetchedSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value }));
|
||||
setSubscriptions(selectAllSubscriptionOption.concat(fetchedSubscriptions));
|
||||
setError(ERROR_SOURCE, undefined);
|
||||
if (query.azureResourceGraph?.scope !== ARGScope.Directory) {
|
||||
datasource
|
||||
.getSubscriptions()
|
||||
.then((results) => {
|
||||
const selectAllSubscriptionOption = [
|
||||
{ label: 'Select all subscriptions', value: 'Select all subscriptions', description: 'Select all' },
|
||||
];
|
||||
const fetchedSubscriptions = results.map((v) => ({ label: v.text, value: v.value, description: v.value }));
|
||||
setSubscriptions(selectAllSubscriptionOption.concat(fetchedSubscriptions));
|
||||
setError(ERROR_SOURCE, undefined);
|
||||
|
||||
onChange({
|
||||
...query,
|
||||
subscriptions: selectSubscriptions(
|
||||
fetchedSubscriptions.map((v) => v.value),
|
||||
query.subscriptions,
|
||||
query.subscription
|
||||
),
|
||||
});
|
||||
})
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
onChange({
|
||||
...query,
|
||||
subscriptions: selectSubscriptions(
|
||||
fetchedSubscriptions.map((v) => v.value),
|
||||
query.subscriptions,
|
||||
query.subscription
|
||||
),
|
||||
});
|
||||
})
|
||||
.catch((err) => setError(ERROR_SOURCE, err));
|
||||
}
|
||||
// We are only interested in re-fetching subscriptions if the data source changes
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [datasource]);
|
||||
}, [datasource, query?.azureResourceGraph?.scope]);
|
||||
|
||||
const onChangeScope = (change: SelectableValue<ARGScope>) => {
|
||||
onChange({
|
||||
...query,
|
||||
azureResourceGraph: {
|
||||
...query.azureResourceGraph,
|
||||
scope: change.value,
|
||||
},
|
||||
subscriptions: [],
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<span data-testid={selectors.components.queryEditor.argsQueryEditor.container.input}>
|
||||
<EditorRows>
|
||||
<EditorRow>
|
||||
<EditorFieldGroup>
|
||||
<SubscriptionField
|
||||
subscriptions={subscriptions}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
setError={setError}
|
||||
/>
|
||||
<Field label={t('components.scope-selector.label', 'Scope')}>
|
||||
<Combobox
|
||||
onChange={onChangeScope}
|
||||
options={[
|
||||
{ value: ARGScope.Directory, label: 'Directory' },
|
||||
{ value: ARGScope.Subscription, label: 'Subscription' },
|
||||
]}
|
||||
value={query.azureResourceGraph?.scope || ARGScope.Subscription}
|
||||
width={20}
|
||||
data-testid={selectors.components.queryEditor.argsQueryEditor.scope.input}
|
||||
/>
|
||||
</Field>
|
||||
{query?.azureResourceGraph?.scope !== ARGScope.Directory ? (
|
||||
<SubscriptionField
|
||||
subscriptions={subscriptions}
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
setError={setError}
|
||||
/>
|
||||
) : null}
|
||||
</EditorFieldGroup>
|
||||
</EditorRow>
|
||||
</EditorRows>
|
||||
|
||||
@@ -271,11 +271,14 @@ composableKinds: DataQuery: {
|
||||
timeFilter?: #BuilderQueryEditorWhereExpressionArray
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
#ARGScope: "subscription" | "directory" @cuetsy(kind="enum", memberNames="Subscription|Directory")
|
||||
#AzureResourceGraphQuery: {
|
||||
// Azure Resource Graph KQL query to be executed.
|
||||
query?: string
|
||||
// Specifies the format results should be returned as. Defaults to table.
|
||||
resultFormat?: string
|
||||
// Specifies the scope of the query. Defaults to subscription.
|
||||
scope?: #ARGScope
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
#AzureMonitorResource: {
|
||||
|
||||
@@ -450,6 +450,11 @@ export interface BuilderQueryExpression {
|
||||
where?: BuilderQueryEditorWhereExpressionArray;
|
||||
}
|
||||
|
||||
export enum ARGScope {
|
||||
Directory = 'directory',
|
||||
Subscription = 'subscription',
|
||||
}
|
||||
|
||||
export interface AzureResourceGraphQuery {
|
||||
/**
|
||||
* Azure Resource Graph KQL query to be executed.
|
||||
@@ -459,6 +464,10 @@ export interface AzureResourceGraphQuery {
|
||||
* Specifies the format results should be returned as. Defaults to table.
|
||||
*/
|
||||
resultFormat?: string;
|
||||
/**
|
||||
* Specifies the scope of the query. Defaults to subscription.
|
||||
*/
|
||||
scope?: ARGScope;
|
||||
}
|
||||
|
||||
export interface AzureMonitorResource {
|
||||
|
||||
@@ -84,6 +84,9 @@ export const components = {
|
||||
container: {
|
||||
input: 'data-testid azure-monitor-arg-query-editor',
|
||||
},
|
||||
scope: {
|
||||
input: 'data-testid azure-monitor-arg-query-editor-scope',
|
||||
},
|
||||
subscriptions: {
|
||||
input: 'data-testid azure-monitor-args-subscription',
|
||||
},
|
||||
|
||||
+3
@@ -222,6 +222,9 @@
|
||||
"text-no-resources": "No resources found",
|
||||
"title-error-occurred": "An error occurred while requesting resources from Azure Monitor"
|
||||
},
|
||||
"scope-selector": {
|
||||
"label": "Scope"
|
||||
},
|
||||
"search": {
|
||||
"aria-label-resource-search": "Resource search",
|
||||
"placeholder-resource-search": "Search for a resource"
|
||||
|
||||
Reference in New Issue
Block a user