mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Azure Monitor: Enable multiple resource queries (#62467)
This commit is contained in:
parent
14dd1be244
commit
6d230d95eb
@ -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 |
|
||||
|
@ -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}');
|
||||
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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'],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
|
@ -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' }],
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
@ -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;
|
@ -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');
|
||||
|
@ -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} />
|
||||
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -246,7 +246,7 @@ const createLogAnalyticsTemplateVariableQuery = async (
|
||||
queryType: AzureQueryType.LogAnalytics,
|
||||
azureLogAnalytics: {
|
||||
query: rawQuery,
|
||||
resources: [resource],
|
||||
resources: resource ? [resource] : [],
|
||||
},
|
||||
subscription: defaultSubscriptionId,
|
||||
};
|
||||
|
@ -219,7 +219,7 @@ describe('migrateStringQueriesToObjectQueries', () => {
|
||||
queryType: AzureQueryType.LogAnalytics,
|
||||
azureLogAnalytics: {
|
||||
query: 'some kind of kql query',
|
||||
resources: [''],
|
||||
resources: [],
|
||||
},
|
||||
subscription: 'defaultSubscriptionId',
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user