Azure Monitor: Adapt ResourceField to multiple resources (#61618)

This commit is contained in:
Andres Martinez Gotor
2023-01-19 09:46:07 +01:00
committed by GitHub
parent faf07716ee
commit 6d9e082f27
7 changed files with 114 additions and 80 deletions
@@ -53,11 +53,7 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
ResourceRowType.Resource,
ResourceRowType.Variable,
]}
resource={
query.azureLogAnalytics?.resources && query.azureLogAnalytics.resources.length
? query.azureLogAnalytics.resources[0]
: ''
}
resources={query.azureLogAnalytics?.resources ?? []}
queryType="logs"
/>
</EditorFieldGroup>
@@ -77,6 +77,41 @@ describe('MetricsQueryEditor', () => {
expect(await screen.findByTestId('azure-monitor-metrics-query-editor-with-experimental-ui')).toBeInTheDocument();
});
it('should show the current resource in the ResourcePicker', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery({
subscription: 'def-456',
azureMonitor: {
metricNamespace: 'Microsoft.Compute/virtualMachines',
resources: [
{
resourceGroup: 'dev-3',
resourceName: 'web-server',
},
],
},
});
const onChange = jest.fn();
render(
<MetricsQueryEditor
data={mockPanelData}
query={query}
datasource={mockDatasource}
variableOptionGroup={variableOptionGroup}
onChange={onChange}
setError={() => {}}
/>
);
const resourcePickerButton = await screen.findByRole('button', { name: 'web-server' });
expect(resourcePickerButton).toBeInTheDocument();
resourcePickerButton.click();
const selection = await screen.findAllByLabelText('web-server');
expect(selection).toHaveLength(2);
});
it('should change resource when a resource is selected in the ResourcePicker', async () => {
const mockDatasource = createMockDatasource({ resourcePickerData: createMockResourcePickerData() });
const query = createMockQuery();
@@ -125,10 +160,10 @@ describe('MetricsQueryEditor', () => {
azureMonitor: expect.objectContaining({
metricNamespace: 'microsoft.compute/virtualmachines',
resources: [
{
expect.objectContaining({
resourceGroup: 'dev-3',
resourceName: 'web-server',
},
}),
],
}),
})
@@ -4,7 +4,7 @@ import { PanelData } from '@grafana/data/src/types';
import { EditorRows, EditorRow, EditorFieldGroup } from '@grafana/experimental';
import type Datasource from '../../datasource';
import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish, AzureMetricResource } from '../../types';
import type { AzureMonitorQuery, AzureMonitorOption, AzureMonitorErrorish } from '../../types';
import ResourceField from '../ResourceField';
import { ResourceRowType } from '../ResourcePicker/types';
@@ -37,13 +37,14 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
const metricsMetadata = useMetricMetadata(query, datasource, onChange);
const metricNamespaces = useMetricNamespaces(query, datasource, onChange, setError);
const metricNames = useMetricNames(query, datasource, onChange, setError);
const resource: AzureMetricResource = {
subscription: query.subscription,
resourceGroup: query.azureMonitor?.resources?.[0]?.resourceGroup,
metricNamespace: query.azureMonitor?.metricNamespace,
resourceName: query.azureMonitor?.resources?.[0]?.resourceName,
region: query.azureMonitor?.region,
};
const resources =
query.azureMonitor?.resources?.map((r) => ({
subscription: query.subscription,
resourceGroup: r.resourceGroup,
metricNamespace: query.azureMonitor?.metricNamespace,
resourceName: r.resourceName,
region: query.azureMonitor?.region,
})) ?? [];
return (
<span data-testid="azure-monitor-metrics-query-editor-with-experimental-ui">
<EditorRows>
@@ -56,7 +57,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
onQueryChange={onChange}
setError={setError}
selectableEntryTypes={[ResourceRowType.Resource]}
resource={resource}
resources={resources ?? []}
queryType={'metrics'}
/>
<MetricNamespaceField
@@ -1,7 +1,7 @@
import { cx } from '@emotion/css';
import React, { useCallback, useEffect, useState } from 'react';
import { Button, Icon, Modal, useStyles2 } from '@grafana/ui';
import { Button, Icon, Modal, useStyles2, IconName } from '@grafana/ui';
import Datasource from '../../datasource';
import { selectors } from '../../e2e/selectors';
@@ -11,12 +11,12 @@ import { Field } from '../Field';
import ResourcePicker from '../ResourcePicker';
import getStyles from '../ResourcePicker/styles';
import { ResourceRowType } from '../ResourcePicker/types';
import { parseResourceDetails, setResource } from '../ResourcePicker/utils';
import { parseMultipleResourceDetails, setResources } from '../ResourcePicker/utils';
interface ResourceFieldProps<T> extends AzureQueryEditorFieldProps {
selectableEntryTypes: ResourceRowType[];
queryType: ResourcePickerQueryType;
resource: T;
resources: T[];
inlineField?: boolean;
labelWidth?: number;
}
@@ -27,7 +27,7 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
onQueryChange,
selectableEntryTypes,
queryType,
resource,
resources,
inlineField,
labelWidth,
}) => {
@@ -43,11 +43,11 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
}, []);
const handleApply = useCallback(
(resource: string | AzureMetricResource | undefined) => {
onQueryChange(setResource(query, resource));
(resources: Array<string | AzureMetricResource>) => {
onQueryChange(setResources(query, queryType, resources));
closePicker();
},
[closePicker, onQueryChange, query]
[closePicker, onQueryChange, query, queryType]
);
return (
@@ -63,9 +63,8 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
>
<ResourcePicker
resourcePickerData={datasource.resourcePickerData}
// TODO: This should be a list of resources, not a single resource
resources={[resource]}
onApply={(resources) => resources && handleApply(resources[0])}
resources={resources}
onApply={handleApply}
onCancel={closePicker}
selectableEntryTypes={selectableEntryTypes}
queryType={queryType}
@@ -73,7 +72,7 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
</Modal>
<Field label="Resource" inlineField={inlineField} labelWidth={labelWidth}>
<Button className={styles.resourceFieldButton} variant="secondary" onClick={handleOpenPicker} type="button">
<ResourceLabel resource={resource} datasource={datasource} />
<ResourceLabel resources={resources} datasource={datasource} />
</Button>
</Field>
</span>
@@ -81,58 +80,53 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
};
interface ResourceLabelProps<T> {
resource: T;
resources: T[];
datasource: Datasource;
}
const ResourceLabel = ({ resource, datasource }: ResourceLabelProps<string | AzureMetricResource>) => {
const [resourceComponents, setResourceComponents] = useState(parseResourceDetails(resource ?? ''));
const ResourceLabel = ({ resources, datasource }: ResourceLabelProps<string | AzureMetricResource>) => {
const [resourcesComponents, setResourcesComponents] = useState(parseMultipleResourceDetails(resources));
useEffect(() => {
if (resource && parseResourceDetails(resource)) {
typeof resource === 'string'
? datasource.resourcePickerData.getResourceURIDisplayProperties(resource).then(setResourceComponents)
: setResourceComponents(resource);
} else {
setResourceComponents({});
}
}, [datasource.resourcePickerData, resource]);
setResourcesComponents(parseMultipleResourceDetails(resources));
}, [resources]);
if (!resource || (typeof resource === 'object' && !resource.subscription)) {
if (!resources.length) {
return <>Select a resource</>;
}
if (resourceComponents) {
return <FormattedResource resource={resourceComponents} />;
}
return <>{resource}</>;
return <FormattedResource resources={resourcesComponents} />;
};
interface FormattedResourceProps {
resource: AzureMetricResource;
resources: AzureMetricResource[];
}
const FormattedResource = ({ resource }: FormattedResourceProps) => {
const FormattedResource = ({ resources }: FormattedResourceProps) => {
const styles = useStyles2(getStyles);
if (resource.resourceName) {
return (
<span className={cx(styles.truncated, styles.resourceField)}>
<Icon name="cube" /> {resource.resourceName.split('/')[0]}
</span>
);
}
if (resource.resourceGroup) {
return (
<span>
<Icon name="folder" /> {resource.resourceGroup}
</span>
);
}
let icon: IconName = 'cube';
const items: string[] = [];
resources.forEach((resource) => {
if (resource.resourceName) {
items.push(resource.resourceName.split('/')[0]);
return;
}
if (resource.resourceGroup) {
icon = 'folder';
items.push(resource.resourceGroup);
return;
}
if (resource.subscription) {
icon = 'layer-group';
items.push(resource.subscription);
return;
}
});
return (
<span>
<Icon name="layer-group" /> {resource.subscription}
<span className={cx(styles.truncated, styles.resourceField)}>
<Icon name={icon} />
{items.join(', ')}
</span>
);
};
@@ -28,7 +28,7 @@ const Advanced = ({ resources, onChange }: ResourcePickerProps<string | AzureMet
isOpen={isAdvancedOpen}
onToggle={() => setIsAdvancedOpen(!isAdvancedOpen)}
>
{resources.map((resource, index) => (
{(resources.length ? resources : [{}]).map((resource, index) => (
<div key={`resource-${index + 1}`}>
{typeof resource === 'string' ? (
<>
@@ -8,7 +8,7 @@ import {
parseResourceDetails,
parseResourceURI,
resourcesToStrings,
setResource,
setResources,
} from './utils';
jest.mock('@grafana/runtime', () => ({
@@ -191,22 +191,24 @@ describe('AzureMonitor ResourcePicker utils', () => {
});
});
describe('setResource', () => {
describe('setResources', () => {
it('updates a resource with a resource URI for Log Analytics', () => {
expect(setResource(createMockQuery(), '/subscription/sub')).toMatchObject({
expect(setResources(createMockQuery(), 'logs', ['/subscription/sub'])).toMatchObject({
azureLogAnalytics: { resources: ['/subscription/sub'] },
});
});
it('updates a resource with a resource parameters for Metrics', () => {
expect(
setResource(createMockQuery(), {
subscription: 'sub',
resourceGroup: 'rg',
metricNamespace: 'Microsoft.Storage/storageAccounts',
resourceName: 'testacct',
region: 'westus',
})
setResources(createMockQuery(), 'metrics', [
{
subscription: 'sub',
resourceGroup: 'rg',
metricNamespace: 'Microsoft.Storage/storageAccounts',
resourceName: 'testacct',
region: 'westus',
},
])
).toMatchObject({
subscription: 'sub',
azureMonitor: {
@@ -3,6 +3,7 @@ import produce from 'immer';
import { getTemplateSrv } from '@grafana/runtime';
import UrlBuilder from '../../azure_monitor/url_builder';
import { ResourcePickerQueryType } from '../../resourcePicker/resourcePickerData';
import { AzureMetricResource, AzureMonitorQuery } from '../../types';
import { ResourceRow, ResourceRowGroup } from './types';
@@ -153,26 +154,31 @@ export function addResources(rows: ResourceRowGroup, targetParentId: string, new
});
}
export function setResource(query: AzureMonitorQuery, resource?: string | AzureMetricResource): AzureMonitorQuery {
if (typeof resource === 'string') {
export function setResources(
query: AzureMonitorQuery,
type: ResourcePickerQueryType,
resources: Array<string | AzureMetricResource>
): AzureMonitorQuery {
if (type === 'logs') {
// Resource URI for LogAnalytics
return {
...query,
azureLogAnalytics: {
...query.azureLogAnalytics,
resources: [resource],
resources: resourcesToStrings(resources),
},
};
}
// Resource object for metrics
const parsedResource = resources.length ? parseResourceDetails(resources[0]) : {};
return {
...query,
subscription: resource?.subscription,
subscription: parsedResource.subscription,
azureMonitor: {
...query.azureMonitor,
metricNamespace: resource?.metricNamespace?.toLocaleLowerCase(),
region: resource?.region,
resources: [{ resourceGroup: resource?.resourceGroup, resourceName: resource?.resourceName }],
metricNamespace: parsedResource.metricNamespace?.toLocaleLowerCase(),
region: parsedResource.region,
resources: parseMultipleResourceDetails(resources),
metricName: undefined,
aggregation: undefined,
timeGrain: '',