Azure Monitor: Implement logic to allow multiple selection (#61740)

This commit is contained in:
Andres Martinez Gotor 2023-01-20 11:19:31 +01:00 committed by GitHub
parent 46722679b5
commit 3e86a1b3d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 325 additions and 8 deletions

View File

@ -139,8 +139,8 @@ e2e.scenario({
scenario: () => {
e2e.flows.addDashboard({
timeRange: {
from: '2022-10-03 00:00:00',
to: '2022-10-03 23:59:59',
from: 'now-6h',
to: 'now',
zone: 'Coordinated Universal Time',
},
});
@ -210,8 +210,8 @@ e2e.scenario({
scenario: () => {
e2e.flows.addDashboard({
timeRange: {
from: '2022-10-03 00:00:00',
to: '2022-10-03 23:59:59',
from: 'now-6h',
to: 'now',
zone: 'Coordinated Universal Time',
},
});

View File

@ -88,4 +88,5 @@ export interface FeatureToggles {
disablePrometheusExemplarSampling?: boolean;
alertingBacktesting?: boolean;
alertingNoNormalState?: boolean;
azureMultipleResourcePicker?: boolean;
}

View File

@ -183,6 +183,7 @@ export const resourceTypeDisplayNames: { [k: string]: string } = {
'microsoft.hpcworkbench/instances/consortiums': 'Consortiums (preview)',
'microsoft.hybridcompute/machines': 'Servers - Azure Arc',
'microsoft.hybridcompute/privatelinkscopes': 'Azure Arc Private Link Scopes',
'microsoft.hybridcontainerservice/provisionedclusters': 'Provisioned clusters',
'microsoft.hybriddata/datamanagers': 'StorSimple Data Managers',
'microsoft.hybridnetwork/devices': 'Azure Network Function Manager Devices',
'microsoft.hybridnetwork/networkfunctions': 'Azure Network Function Manager Network Functions',
@ -269,6 +270,7 @@ export const resourceTypeDisplayNames: { [k: string]: string } = {
'microsoft.network/networkmanagers': 'Network Managers',
'microsoft.network/networksecuritygroups': 'Network security groups',
'microsoft.network/networkwatchers': 'Network Watchers',
'microsoft.network/networkwatchers/connectionmonitors': 'Connection Monitors',
'microsoft.network/networkwatchers/flowlogs': 'NSG Flow Logs',
'microsoft.network/privatednszones': 'Private DNS zones',
'microsoft.network/privateendpoints': 'Private endpoints',
@ -400,3 +402,23 @@ export const resourceTypeDisplayNames: { [k: string]: string } = {
'microsoft.security/insights/classification': 'Data Sensitivity Security Insights (Preview)',
'microsoft.security/locations/alerts': 'Security Alerts',
};
// This list has been manually written using the Azure Portal as the source.
// Visit https://portal.azure.com/#view/Microsoft_Azure_Monitoring/AzureMonitoringBrowseBlade/~/metrics
// and go to Select a scope > Resource types > Multi-resource compatible (preview)
export const multiResourceCompatibleTypes: { [ns: string]: boolean } = {
'microsoft.cache/redis': true, // 'Azure Cache for Redis'
'microsoft.dbforpostgresql/flexibleservers': true, // 'Azure Database for PostgreSQL flexible servers'
'microsoft.storagecache/amlfilesystems': true, // 'Lustre File Systems'
'microsoft.databoxedge/databoxedgedevices': true, // 'Azure Stack Edge / Data Box Gateway'
'microsoft.dataprotection/backupvaults': true, // 'Backup vaults'
'microsoft.netapp/netappaccounts/capacitypools': true, // 'Capacity pools'
'microsoft.network/networkwatchers/connectionmonitors': true, // 'Connection Monitors'
'microsoft.keyvault/vaults': true, // 'Key vaults'
'microsoft.recoveryservices/vaults': true, // 'Recovery Services vaults'
'microsoft.sql/servers/databases': true, // 'SQL databases'
'microsoft.sql/servers/elasticpools': true, // 'SQL elastic pools'
'microsoft.compute/virtualmachinescalesets': true, // 'Virtual machine scale sets'
'microsoft.compute/virtualmachines': true, // 'Virtual machines'
'microsoft.signalrservice/webpubsub': true, // 'Web PubSub Service'
};

View File

@ -0,0 +1,121 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import config from 'app/core/config';
import createMockDatasource from '../../__mocks__/datasource';
import createMockQuery from '../../__mocks__/query';
import { createMockResourcePickerData } from '../MetricsQueryEditor/MetricsQueryEditor.test';
import LogsQueryEditor from './LogsQueryEditor';
jest.mock('@grafana/runtime', () => ({
...(jest.requireActual('@grafana/runtime') as unknown as object),
getTemplateSrv: () => ({
replace: (val: string) => {
return val;
},
}),
}));
const variableOptionGroup = {
label: 'Template variables',
options: [],
};
beforeEach(() => {
config.featureToggles.azureMultipleResourcePicker = true;
});
describe('LogsQueryEdiutor', () => {
const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = function () {};
});
afterEach(() => {
window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
});
it('should select multiple resources', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.subscription;
delete query?.azureLogAnalytics?.resources;
const onChange = jest.fn();
render(
<LogsQueryEditor
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('web-server');
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
const checkbox2 = await screen.findByLabelText('db-server');
await userEvent.click(checkbox2);
expect(checkbox2).toBeChecked();
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
resources: [
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server',
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server',
],
}),
})
);
});
it('should disable other resource types when selecting multiple resources', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.subscription;
delete query?.azureLogAnalytics?.resources;
const onChange = jest.fn();
render(
<LogsQueryEditor
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('web-server');
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
expect(await screen.findByLabelText('web-server_DataDisk')).toBeDisabled();
});
});

View File

@ -1,12 +1,14 @@
import React from 'react';
import { EditorFieldGroup, EditorRow, EditorRows } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { Alert } from '@grafana/ui';
import Datasource from '../../datasource';
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
import ResourceField from '../ResourceField';
import { ResourceRowType } from '../ResourcePicker/types';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types';
import { parseResourceDetails } from '../ResourcePicker/utils';
import FormatAsField from './FormatAsField';
import QueryField from './QueryField';
@ -32,6 +34,23 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
hideFormatAs,
}) => {
const migrationError = useMigrations(datasource, query, onChange);
const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => {
if (selectedRows.length === 0) {
// Only if there is some resource(s) selected we should disable rows
return false;
}
// Disable multiple selection until the feature is ready
if (!config.featureToggles.azureMultipleResourcePicker) {
return true;
}
const rowResourceNS = parseResourceDetails(row.uri, row.location).metricNamespace?.toLowerCase();
const selectedRowSampleNs = parseResourceDetails(
selectedRows[0].uri,
selectedRows[0].location
).metricNamespace?.toLowerCase();
// Only resources with the same metricNamespace can be selected
return rowResourceNS !== selectedRowSampleNs;
};
return (
<span data-testid="azure-monitor-logs-query-editor-with-experimental-ui">
@ -55,6 +74,7 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
]}
resources={query.azureLogAnalytics?.resources ?? []}
queryType="logs"
disableRow={disableRow}
/>
</EditorFieldGroup>
</EditorRow>

View File

@ -3,6 +3,8 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import { selectOptionInTest } from 'test/helpers/selectOptionInTest';
import config from 'app/core/config';
import createMockDatasource from '../../__mocks__/datasource';
import { createMockInstanceSetttings } from '../../__mocks__/instanceSettings';
import createMockPanelData from '../../__mocks__/panelData';
@ -31,6 +33,10 @@ const variableOptionGroup = {
options: [],
};
beforeEach(() => {
config.featureToggles.azureMultipleResourcePicker = true;
});
export function createMockResourcePickerData() {
const mockDatasource = createMockDatasource();
const mockResourcePicker = new ResourcePickerData(
@ -170,6 +176,100 @@ describe('MetricsQueryEditor', () => {
);
});
it('should select multiple resources', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.subscription;
delete query?.azureMonitor?.resources;
delete query?.azureMonitor?.metricNamespace;
const onChange = jest.fn();
render(
<MetricsQueryEditor
data={mockPanelData}
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('web-server');
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
const checkbox2 = await screen.findByLabelText('db-server');
await userEvent.click(checkbox2);
expect(checkbox2).toBeChecked();
await userEvent.click(await screen.findByRole('button', { name: 'Apply' }));
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
subscription: 'def-456',
azureMonitor: expect.objectContaining({
metricNamespace: 'microsoft.compute/virtualmachines',
resources: [
expect.objectContaining({
resourceGroup: 'dev-3',
resourceName: 'web-server',
}),
expect.objectContaining({
resourceGroup: 'dev-3',
resourceName: 'db-server',
}),
],
}),
})
);
});
it('should disable other resource types when selecting multiple resources', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
delete query?.subscription;
delete query?.azureMonitor?.resources;
delete query?.azureMonitor?.metricNamespace;
const onChange = jest.fn();
render(
<MetricsQueryEditor
data={mockPanelData}
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: 'Select a resource' });
resourcePickerButton.click();
const subscriptionButton = await screen.findByRole('button', { name: 'Expand Primary Subscription' });
subscriptionButton.click();
const resourceGroupButton = await screen.findByRole('button', { name: 'Expand A Great Resource Group' });
resourceGroupButton.click();
const checkbox = await screen.findByLabelText('web-server');
await userEvent.click(checkbox);
expect(checkbox).toBeChecked();
expect(await screen.findByLabelText('web-server_DataDisk')).toBeDisabled();
});
it('should change the metric name when selected', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const onChange = jest.fn();

View File

@ -2,11 +2,14 @@ import React from 'react';
import { PanelData } from '@grafana/data/src/types';
import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental';
import { config } from '@grafana/runtime';
import { multiResourceCompatibleTypes } from '../../azureMetadata';
import type Datasource from '../../datasource';
import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types';
import ResourceField from '../ResourceField';
import { ResourceRowType } from '../ResourcePicker/types';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types';
import { parseResourceDetails } from '../ResourcePicker/utils';
import AggregationField from './AggregationField';
import DimensionFields from './DimensionFields';
@ -45,6 +48,31 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
resourceName: r.resourceName,
region: query.azureMonitor?.region,
})) ?? [];
const disableRow = (row: ResourceRow, selectedRows: ResourceRowGroup) => {
if (selectedRows.length === 0) {
// Only if there is some resource(s) selected we should disable rows
return false;
}
if (!config.featureToggles.azureMultipleResourcePicker) {
// Disable multiple selection until the feature is ready
return true;
}
const rowResource = parseResourceDetails(row.uri, row.location);
const selectedRowSample = parseResourceDetails(selectedRows[0].uri, selectedRows[0].location);
// Only resources:
// - in the same subscription
// - in the same region
// - with the same metric namespace
// - with a metric namespace that is compatible with multi-resource queries
return (
rowResource.subscription !== selectedRowSample.subscription ||
rowResource.region !== selectedRowSample.region ||
rowResource.metricNamespace?.toLocaleLowerCase() !== selectedRowSample.metricNamespace?.toLocaleLowerCase() ||
!multiResourceCompatibleTypes[rowResource.metricNamespace?.toLocaleLowerCase() ?? '']
);
};
return (
<span data-testid="azure-monitor-metrics-query-editor-with-experimental-ui">
<EditorRows>
@ -59,6 +87,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
selectableEntryTypes={[ResourceRowType.Resource]}
resources={resources ?? []}
queryType={'metrics'}
disableRow={disableRow}
/>
<MetricNamespaceField
metricNamespaces={metricNamespaces}

View File

@ -10,7 +10,7 @@ import { AzureQueryEditorFieldProps, AzureMetricResource } from '../../types';
import { Field } from '../Field';
import ResourcePicker from '../ResourcePicker';
import getStyles from '../ResourcePicker/styles';
import { ResourceRowType } from '../ResourcePicker/types';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../ResourcePicker/types';
import { parseMultipleResourceDetails, setResources } from '../ResourcePicker/utils';
interface ResourceFieldProps<T> extends AzureQueryEditorFieldProps {
@ -19,6 +19,7 @@ interface ResourceFieldProps<T> extends AzureQueryEditorFieldProps {
resources: T[];
inlineField?: boolean;
labelWidth?: number;
disableRow: (row: ResourceRow, selectedRows: ResourceRowGroup) => boolean;
}
const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>> = ({
@ -30,6 +31,7 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
resources,
inlineField,
labelWidth,
disableRow,
}) => {
const styles = useStyles2(getStyles);
const [pickerIsOpen, setPickerIsOpen] = useState(false);
@ -68,6 +70,7 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
onCancel={closePicker}
selectableEntryTypes={selectableEntryTypes}
queryType={queryType}
disableRow={disableRow}
/>
</Modal>
<Field label="Resource" inlineField={inlineField} labelWidth={labelWidth}>

View File

@ -18,6 +18,7 @@ const defaultProps = {
onRowSelectedChange: jest.fn(),
selectableEntryTypes: [],
scrollIntoView: false,
disableRow: jest.fn().mockReturnValue(false),
};
describe('NestedRow', () => {
@ -56,4 +57,16 @@ describe('NestedRow', () => {
const box = screen.queryByRole('checkbox');
expect(box).not.toBeInTheDocument();
});
it('should disable a checkbox if specified', () => {
render(
<table>
<tbody>
<NestedRow {...defaultProps} selectableEntryTypes={[ResourceRowType.Resource]} disableRow={() => true} />
</tbody>
</table>
);
const box = screen.queryByRole('checkbox');
expect(box).toBeDisabled();
});
});

View File

@ -15,6 +15,7 @@ interface NestedRowProps {
requestNestedRows: (row: ResourceRow) => Promise<void>;
onRowSelectedChange: (row: ResourceRow, selected: boolean) => void;
selectableEntryTypes: ResourceRowType[];
disableRow: (row: ResourceRow, selectedRows: ResourceRowGroup) => boolean;
scrollIntoView?: boolean;
}
@ -26,12 +27,13 @@ const NestedRow: React.FC<NestedRowProps> = ({
onRowSelectedChange,
selectableEntryTypes,
scrollIntoView,
disableRow,
}) => {
const styles = useStyles2(getStyles);
const [rowStatus, setRowStatus] = useState<'open' | 'closed' | 'loading'>('closed');
const isSelected = !!selectedRows.find((v) => v.uri === row.uri);
const isDisabled = selectedRows.length > 0 && !isSelected;
const isDisabled = !isSelected && disableRow(row, selectedRows);
const isOpen = rowStatus === 'open';
const onRowToggleCollapse = async () => {
@ -92,6 +94,7 @@ const NestedRow: React.FC<NestedRowProps> = ({
onRowSelectedChange={onRowSelectedChange}
selectableEntryTypes={selectableEntryTypes}
scrollIntoView={scrollIntoView}
disableRow={disableRow}
/>
))}

View File

@ -70,6 +70,7 @@ const defaultProps = {
ResourceRowType.Variable,
],
queryType,
disableRow: jest.fn(),
};
describe('AzureMonitor ResourcePicker', () => {

View File

@ -25,6 +25,7 @@ interface ResourcePickerProps<T> {
onApply: (resources: T[]) => void;
onCancel: () => void;
disableRow: (row: ResourceRow, selectedRows: ResourceRowGroup) => boolean;
}
const ResourcePicker = ({
@ -34,6 +35,7 @@ const ResourcePicker = ({
onCancel,
selectableEntryTypes,
queryType,
disableRow,
}: ResourcePickerProps<string | AzureMetricResource>) => {
const styles = useStyles2(getStyles);
@ -203,6 +205,7 @@ const ResourcePicker = ({
onRowSelectedChange={handleSelectionChanged}
selectableEntryTypes={selectableEntryTypes}
scrollIntoView={true}
disableRow={disableRow}
/>
))}
</tbody>
@ -226,6 +229,7 @@ const ResourcePicker = ({
requestNestedRows={requestNestedRows}
onRowSelectedChange={handleSelectionChanged}
selectableEntryTypes={selectableEntryTypes}
disableRow={() => false}
/>
))}
</tbody>