Azure Monitor: Enable multiple resource queries (#62467)

This commit is contained in:
Andres Martinez Gotor 2023-01-30 17:19:03 +01:00 committed by GitHub
parent 14dd1be244
commit 6d230d95eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 159 additions and 352 deletions

View File

@ -98,24 +98,22 @@ Alpha features might be changed or removed without prior notice.
| `sessionRemoteCache` | Enable using remote cache for user sessions |
| `alertingBacktesting` | Rule backtesting API for alerting |
| `editPanelCSVDragAndDrop` | Enables drag and drop for CSV and Excel files |
| `azureMultipleResourcePicker` | Azure multiple resource picker |
| `logsContextDatasourceUi` | Allow datasource to provide custom UI for context view |
## Development feature toggles
The following toggles require explicitly setting Grafana's [app mode]({{< relref "../_index.md/#app_mode" >}}) to 'development' before you can enable this feature toggle. These features tend to be experimental.
| Feature toggle name | Description |
| -------------------------------------- | ----------------------------------------------------------------------- |
| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI |
| `showFeatureFlagsInUI` | Show feature flags in the settings UI |
| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails |
| `k8s` | Explore native k8s integrations |
| `k8sDashboards` | Save dashboards via k8s |
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
| `export` | Export grafana instance (to git, etc) |
| `azureMonitorResourcePickerForMetrics` | New UI for Azure Monitor Metrics Query |
| `grpcServer` | Run GRPC server |
| `entityStore` | SQL-based entity store (requires storage flag also) |
| `queryLibrary` | Reusable query library |
| `nestedFolders` | Enable folder nesting |
| Feature toggle name | Description |
| ------------------------------ | ----------------------------------------------------------------------- |
| `dashboardPreviewsAdmin` | Manage the dashboard previews crawler process from the UI |
| `showFeatureFlagsInUI` | Show feature flags in the settings UI |
| `publicDashboardsEmailSharing` | Allows public dashboard sharing to be restricted to only allowed emails |
| `k8s` | Explore native k8s integrations |
| `k8sDashboards` | Save dashboards via k8s |
| `dashboardsFromStorage` | Load dashboards from the generic storage interface |
| `export` | Export grafana instance (to git, etc) |
| `grpcServer` | Run GRPC server |
| `entityStore` | SQL-based entity store (requires storage flag also) |
| `queryLibrary` | Reusable query library |
| `nestedFolders` | Enable folder nesting |

View File

@ -248,6 +248,8 @@ e2e.scenario({
.parent()
.find('input')
.type('microsoft.storage/storageaccounts{downArrow}{enter}');
e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('button').click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels('region').parent().find('input').type('uk south{downArrow}{enter}');
e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource').parent().find('button').click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels('resource')
.parent()
@ -262,8 +264,7 @@ 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.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

@ -47,7 +47,6 @@ export interface FeatureToggles {
supportBundles?: boolean;
dashboardsFromStorage?: boolean;
export?: boolean;
azureMonitorResourcePickerForMetrics?: boolean;
exploreMixedDatasource?: boolean;
tracing?: boolean;
commandPalette?: boolean;
@ -90,7 +89,6 @@ export interface FeatureToggles {
alertingBacktesting?: boolean;
editPanelCSVDragAndDrop?: boolean;
alertingNoNormalState?: boolean;
azureMultipleResourcePicker?: boolean;
topNavCommandPalette?: boolean;
logsSampleInExplore?: boolean;
logsContextDatasourceUi?: boolean;

View File

@ -177,13 +177,6 @@ var (
State: FeatureStateAlpha,
RequiresDevMode: true,
},
{
Name: "azureMonitorResourcePickerForMetrics",
Description: "New UI for Azure Monitor Metrics Query",
State: FeatureStateAlpha,
RequiresDevMode: true,
FrontendOnly: true,
},
{
Name: "exploreMixedDatasource",
Description: "Enable mixed datasource in Explore",
@ -416,11 +409,6 @@ var (
State: FeatureStateBeta,
RequiresRestart: false,
},
{
Name: "azureMultipleResourcePicker",
Description: "Azure multiple resource picker",
State: FeatureStateAlpha,
},
{
Name: "topNavCommandPalette",
Description: "Launch the Command Palette from the top navigation search box",

View File

@ -131,10 +131,6 @@ const (
// Export grafana instance (to git, etc)
FlagExport = "export"
// FlagAzureMonitorResourcePickerForMetrics
// New UI for Azure Monitor Metrics Query
FlagAzureMonitorResourcePickerForMetrics = "azureMonitorResourcePickerForMetrics"
// FlagExploreMixedDatasource
// Enable mixed datasource in Explore
FlagExploreMixedDatasource = "exploreMixedDatasource"
@ -303,10 +299,6 @@ const (
// Stop maintaining state of alerts that are not firing
FlagAlertingNoNormalState = "alertingNoNormalState"
// FlagAzureMultipleResourcePicker
// Azure multiple resource picker
FlagAzureMultipleResourcePicker = "azureMultipleResourcePicker"
// FlagTopNavCommandPalette
// Launch the Command Palette from the top navigation search box
FlagTopNavCommandPalette = "topNavCommandPalette"

View File

@ -2,8 +2,6 @@ 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';
@ -24,10 +22,6 @@ const variableOptionGroup = {
options: [],
};
beforeEach(() => {
config.featureToggles.azureMultipleResourcePicker = true;
});
describe('LogsQueryEdiutor', () => {
const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
@ -151,4 +145,43 @@ describe('LogsQueryEdiutor', () => {
expect(await screen.findByText('You may only choose items of the same resource type.')).toBeInTheDocument();
});
it('should call onApply with a new subscription uri when a user types it in the selection box', 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 advancedSection = screen.getByText('Advanced');
advancedSection.click();
const advancedInput = await screen.findByTestId('input-advanced-resource-picker-1');
// const advancedInput = await screen.findByLabelText('Resource URI(s)');
await userEvent.type(advancedInput, '/subscriptions/def-123');
const applyButton = screen.getByRole('button', { name: 'Apply' });
applyButton.click();
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureLogAnalytics: expect.objectContaining({
resources: ['/subscriptions/def-123'],
}),
})
);
});
});

View File

@ -1,7 +1,6 @@
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';
@ -40,10 +39,6 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
// 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,
@ -82,11 +77,7 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
// eslint-disable-next-line
<AdvancedResourcePicker resources={resources as string[]} onChange={onChange} />
)}
selectionNotice={() =>
config.featureToggles.azureMultipleResourcePicker
? 'You may only choose items of the same resource type.'
: ''
}
selectionNotice={() => 'You may only choose items of the same resource type.'}
/>
</EditorFieldGroup>
</EditorRow>

View File

@ -3,8 +3,6 @@ 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';
@ -33,10 +31,6 @@ const variableOptionGroup = {
options: [],
};
beforeEach(() => {
config.featureToggles.azureMultipleResourcePicker = true;
});
export function createMockResourcePickerData() {
const mockDatasource = createMockDatasource();
const mockResourcePicker = new ResourcePickerData(
@ -378,4 +372,94 @@ describe('MetricsQueryEditor', () => {
},
});
});
it('should show unselect a resource if the value is manually edited', 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 advancedSection = screen.getByText('Advanced');
advancedSection.click();
const advancedInput = await screen.findByLabelText('Subscription');
await userEvent.type(advancedInput, 'def-123');
const updatedCheckboxes = await screen.findAllByLabelText('web-server');
expect(updatedCheckboxes.length).toBe(1);
expect(updatedCheckboxes[0]).not.toBeChecked();
});
it('should call onApply with a new subscription when a user types it in the selection box', 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 advancedSection = screen.getByText('Advanced');
advancedSection.click();
const advancedInput = await screen.findByLabelText('Subscription');
await userEvent.type(advancedInput, 'def-123');
const nsInput = await screen.findByLabelText('Namespace');
await userEvent.type(nsInput, 'ns');
const rgInput = await screen.findByLabelText('Resource Group');
await userEvent.type(rgInput, 'rg');
const rnInput = await screen.findByLabelText('Resource Name');
await userEvent.type(rnInput, 'rn');
const applyButton = screen.getByRole('button', { name: 'Apply' });
applyButton.click();
expect(onChange).toBeCalledTimes(1);
expect(onChange).toBeCalledWith(
expect.objectContaining({
azureMonitor: expect.objectContaining({
resources: [{ subscription: 'def-123', metricNamespace: 'ns', resourceGroup: 'rg', resourceName: 'rn' }],
}),
})
);
});
});

View File

@ -2,7 +2,6 @@ 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';
@ -59,10 +58,6 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
// 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);
@ -80,7 +75,7 @@ const MetricsQueryEditor: React.FC<MetricsQueryEditorProps> = ({
};
const selectionNotice = (selectedRows: ResourceRowGroup) => {
if (selectedRows.length === 0 || !config.featureToggles.azureMultipleResourcePicker) {
if (selectedRows.length === 0) {
return '';
}
const selectedRowSample = parseResourceDetails(selectedRows[0].uri, selectedRows[0].location);

View File

@ -1,44 +0,0 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import Advanced from './Advanced';
describe('AzureMonitor ResourcePicker', () => {
it('should set a parameter as an object', async () => {
const onChange = jest.fn();
const { rerender } = render(<Advanced onChange={onChange} resources={[{}]} />);
const advancedSection = screen.getByText('Advanced');
advancedSection.click();
const subsInput = await screen.findByLabelText('Subscription');
await userEvent.type(subsInput, 'd');
expect(onChange).toHaveBeenCalledWith([{ subscription: 'd' }]);
rerender(<Advanced onChange={onChange} resources={[{ subscription: 'def-123' }]} />);
expect(screen.getByLabelText('Subscription').outerHTML).toMatch('value="def-123"');
});
it('should set a parameter as uri', async () => {
const onChange = jest.fn();
const { rerender } = render(<Advanced onChange={onChange} resources={['']} />);
const advancedSection = screen.getByText('Advanced');
advancedSection.click();
const subsInput = await screen.findByLabelText('Resource URI');
await userEvent.type(subsInput, '/');
expect(onChange).toHaveBeenCalledWith(['/']);
rerender(<Advanced onChange={onChange} resources={['/subscriptions/sub']} />);
expect(screen.getByLabelText('Resource URI').outerHTML).toMatch('value="/subscriptions/sub"');
});
it('should render multiple resources', async () => {
render(<Advanced onChange={jest.fn()} resources={['/subscriptions/sub1', '/subscriptions/sub2']} />);
const advancedSection = screen.getByText('Advanced');
advancedSection.click();
expect(screen.getByDisplayValue('/subscriptions/sub1')).toBeInTheDocument();
expect(screen.getByDisplayValue('/subscriptions/sub2')).toBeInTheDocument();
});
});

View File

@ -1,146 +0,0 @@
import React, { useState } from 'react';
import { Icon, Input, Tooltip, Collapse, Label, InlineField } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
import { AzureMetricResource } from '../../types';
import { Space } from '../Space';
interface ResourcePickerProps<T> {
resources: T[];
onChange: (resources: T[]) => void;
}
const Advanced = ({ resources, onChange }: ResourcePickerProps<string | AzureMetricResource>) => {
const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resources.length && JSON.stringify(resources).includes('$'));
const onResourceChange = (resource: string | AzureMetricResource, index: number) => {
const newResources = [...resources];
newResources[index] = resource;
onChange(newResources);
};
return (
<div data-testid={selectors.components.queryEditor.resourcePicker.advanced.collapse}>
<Collapse
collapsible
label="Advanced"
isOpen={isAdvancedOpen}
onToggle={() => setIsAdvancedOpen(!isAdvancedOpen)}
>
{(resources.length ? resources : [{}]).map((resource, index) => (
<div key={`resource-${index + 1}`}>
{typeof resource === 'string' ? (
<>
<Label htmlFor={`input-advanced-resource-picker-${index + 1}`}>
<h6>
Resource URI{' '}
<Tooltip
content={
<>
Manually edit the{' '}
<a
href="https://docs.microsoft.com/en-us/azure/azure-monitor/logs/log-standard-columns#_resourceid"
rel="noopener noreferrer"
target="_blank"
>
resource uri.{' '}
</a>
Supports the use of multiple template variables (ex: /subscriptions/$subId/resourceGroups/$rg)
</>
}
placement="right"
interactive={true}
>
<Icon name="info-circle" />
</Tooltip>
</h6>
</Label>
<Input
id={`input-advanced-resource-picker-${index + 1}`}
value={resource}
onChange={(event) => onResourceChange(event.currentTarget.value, index)}
placeholder="ex: /subscriptions/$subId"
/>
</>
) : (
<>
<InlineField
label="Subscription"
grow
transparent
htmlFor={`input-advanced-resource-picker-subscription-${index + 1}`}
labelWidth={15}
data-testid={selectors.components.queryEditor.resourcePicker.advanced.subscription.input}
>
<Input
id={`input-advanced-resource-picker-subscription-${index + 1}`}
value={resource?.subscription ?? ''}
onChange={(event) =>
onResourceChange({ ...resource, subscription: event.currentTarget.value }, index)
}
placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee"
/>
</InlineField>
<InlineField
label="Resource Group"
grow
transparent
htmlFor={`input-advanced-resource-picker-resourceGroup-${index + 1}`}
labelWidth={15}
data-testid={selectors.components.queryEditor.resourcePicker.advanced.resourceGroup.input}
>
<Input
id={`input-advanced-resource-picker-resourceGroup-${index + 1}`}
value={resource?.resourceGroup ?? ''}
onChange={(event) =>
onResourceChange({ ...resource, resourceGroup: event.currentTarget.value }, index)
}
placeholder="resource-group"
/>
</InlineField>
<InlineField
label="Namespace"
grow
transparent
htmlFor={`input-advanced-resource-picker-metricNamespace-${index + 1}`}
labelWidth={15}
data-testid={selectors.components.queryEditor.resourcePicker.advanced.namespace.input}
>
<Input
id={`input-advanced-resource-picker-metricNamespace-${index + 1}`}
value={resource?.metricNamespace ?? ''}
onChange={(event) =>
onResourceChange({ ...resource, metricNamespace: event.currentTarget.value }, index)
}
placeholder="Microsoft.Insights/metricNamespaces"
/>
</InlineField>
<InlineField
label="Resource Name"
grow
transparent
htmlFor={`input-advanced-resource-picker-resourceName-${index + 1}`}
labelWidth={15}
data-testid={selectors.components.queryEditor.resourcePicker.advanced.resource.input}
>
<Input
id={`input-advanced-resource-picker-resourceName-${index + 1}`}
value={resource?.resourceName ?? ''}
onChange={(event) =>
onResourceChange({ ...resource, resourceName: event.currentTarget.value }, index)
}
placeholder="name"
/>
</InlineField>
</>
)}
</div>
))}
<Space v={2} />
</Collapse>
</div>
);
};
export default Advanced;

View File

@ -232,85 +232,6 @@ describe('AzureMonitor ResourcePicker', () => {
expect(onApply).toBeCalledWith([]);
});
it('should call onApply with a new subscription uri when a user types it in the selection box', async () => {
const onApply = jest.fn();
render(<ResourcePicker {...defaultProps} onApply={onApply} resources={['']} />);
const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
expect(subscriptionCheckbox).toBeInTheDocument();
expect(subscriptionCheckbox).not.toBeChecked();
const advancedSection = screen.getByText('Advanced');
advancedSection.click();
const advancedInput = await screen.findByLabelText('Resource URI');
await userEvent.type(advancedInput, '/subscriptions/def-123');
const applyButton = screen.getByRole('button', { name: 'Apply' });
applyButton.click();
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith(['/subscriptions/def-123']);
});
it('should call onApply with a new subscription when a user types it in the selection box', async () => {
const onApply = jest.fn();
render(<ResourcePicker {...defaultProps} queryType={'metrics'} onApply={onApply} resources={[{}]} />);
const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
expect(subscriptionCheckbox).toBeInTheDocument();
expect(subscriptionCheckbox).not.toBeChecked();
const advancedSection = screen.getByText('Advanced');
advancedSection.click();
const advancedInput = await screen.findByLabelText('Subscription');
await userEvent.type(advancedInput, 'def-123');
const nsInput = await screen.findByLabelText('Namespace');
await userEvent.type(nsInput, 'ns');
const rgInput = await screen.findByLabelText('Resource Group');
await userEvent.type(rgInput, 'rg');
const rnInput = await screen.findByLabelText('Resource Name');
await userEvent.type(rnInput, 'rn');
const applyButton = screen.getByRole('button', { name: 'Apply' });
applyButton.click();
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith([
{ subscription: 'def-123', metricNamespace: 'ns', resourceGroup: 'rg', resourceName: 'rn' },
]);
});
it('should show unselect a subscription if the value is manually edited', async () => {
render(
<ResourcePicker
{...defaultProps}
resources={[
{
metricNamespace: 'Microsoft.Compute/virtualMachines',
region: 'northeurope',
resourceGroup: 'dev-3',
resourceName: 'web-server',
subscription: 'def-456',
},
]}
/>
);
const checkboxes = await screen.findAllByLabelText('web-server');
expect(checkboxes.length).toBe(2);
expect(checkboxes[0]).toBeChecked();
expect(checkboxes[1]).toBeChecked();
const advancedSection = screen.getByText('Advanced');
advancedSection.click();
const advancedInput = await screen.findByLabelText('Subscription');
await userEvent.type(advancedInput, 'def-123');
const updatedCheckboxes = await screen.findAllByLabelText('web-server');
expect(updatedCheckboxes.length).toBe(1);
expect(updatedCheckboxes[0]).not.toBeChecked();
});
it('renders a search field which show search results when there are results', async () => {
render(<ResourcePicker {...defaultProps} />);
const searchRow1 = screen.queryByLabelText('search-result');

View File

@ -2,7 +2,6 @@ import { cx } from '@emotion/css';
import React, { useCallback, useEffect, useState } from 'react';
import { useEffectOnce } from 'react-use';
import { config } from '@grafana/runtime';
import { Alert, Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import { selectors } from '../../e2e/selectors';
@ -11,7 +10,6 @@ import { AzureMetricResource } from '../../types';
import messageFromError from '../../utils/messageFromError';
import { Space } from '../Space';
import Advanced from './Advanced';
import AdvancedMulti from './AdvancedMulti';
import NestedRow from './NestedRow';
import Search from './Search';
@ -123,7 +121,7 @@ const ResourcePicker = ({
if (isSelected) {
const newRes = queryType === 'logs' ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0];
const newSelected = internalSelected ? internalSelected.concat(newRes) : [newRes];
setInternalSelected(newSelected);
setInternalSelected(newSelected.filter((r) => isValid(r)));
} else {
const newInternalSelected = internalSelected?.filter((r) => {
return !matchURI(resourceToString(r), row.uri);
@ -252,15 +250,11 @@ const ResourcePicker = ({
</>
)}
{config.featureToggles.azureMultipleResourcePicker ? (
<AdvancedMulti
resources={internalSelected}
onChange={(r) => setInternalSelected(r)}
renderAdvanced={renderAdvanced}
/>
) : (
<Advanced resources={internalSelected} onChange={(r) => setInternalSelected(r)} />
)}
<AdvancedMulti
resources={internalSelected}
onChange={(r) => setInternalSelected(r)}
renderAdvanced={renderAdvanced}
/>
<Space v={2} />

View File

@ -1,4 +1,4 @@
import { get } from 'lodash';
import { get, isEqual } from 'lodash';
import React, { useEffect, useState } from 'react';
import { useEffectOnce } from 'react-use';
@ -65,7 +65,9 @@ const VariableEditor = (props: Props) => {
useEffect(() => {
migrateQuery(query, { datasource: datasource }).then((migratedQuery) => {
onChange(migratedQuery);
if (!isEqual(query, migratedQuery)) {
onChange(migratedQuery);
}
});
}, [query, datasource, onChange]);

View File

@ -246,7 +246,7 @@ const createLogAnalyticsTemplateVariableQuery = async (
queryType: AzureQueryType.LogAnalytics,
azureLogAnalytics: {
query: rawQuery,
resources: [resource],
resources: resource ? [resource] : [],
},
subscription: defaultSubscriptionId,
};

View File

@ -219,7 +219,7 @@ describe('migrateStringQueriesToObjectQueries', () => {
queryType: AzureQueryType.LogAnalytics,
azureLogAnalytics: {
query: 'some kind of kql query',
resources: [''],
resources: [],
},
subscription: 'defaultSubscriptionId',
},