mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
AzureMonitor: Adapt ResourcePicker and Advanced components to multiple resources (#61605)
This commit is contained in:
parent
004705a10b
commit
1b86a49622
@ -63,8 +63,9 @@ const ResourceField: React.FC<ResourceFieldProps<string | AzureMetricResource>>
|
||||
>
|
||||
<ResourcePicker
|
||||
resourcePickerData={datasource.resourcePickerData}
|
||||
resource={resource}
|
||||
onApply={handleApply}
|
||||
// TODO: This should be a list of resources, not a single resource
|
||||
resources={[resource]}
|
||||
onApply={(resources) => resources && handleApply(resources[0])}
|
||||
onCancel={closePicker}
|
||||
selectableEntryTypes={selectableEntryTypes}
|
||||
queryType={queryType}
|
||||
|
@ -7,29 +7,38 @@ 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} resource={{}} />);
|
||||
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' });
|
||||
expect(onChange).toHaveBeenCalledWith([{ subscription: 'd' }]);
|
||||
|
||||
rerender(<Advanced onChange={onChange} resource={{ subscription: 'def-123' }} />);
|
||||
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} resource={''} />);
|
||||
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('/');
|
||||
expect(onChange).toHaveBeenCalledWith(['/']);
|
||||
|
||||
rerender(<Advanced onChange={onChange} resource={'/subscriptions/sub'} />);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
@ -7,12 +7,18 @@ import { AzureMetricResource } from '../../types';
|
||||
import { Space } from '../Space';
|
||||
|
||||
interface ResourcePickerProps<T> {
|
||||
resource: T;
|
||||
onChange: (resource: T) => void;
|
||||
resources: T[];
|
||||
onChange: (resources: T[]) => void;
|
||||
}
|
||||
|
||||
const Advanced = ({ resource, onChange }: ResourcePickerProps<string | AzureMetricResource>) => {
|
||||
const [isAdvancedOpen, setIsAdvancedOpen] = useState(!!resource && JSON.stringify(resource).includes('$'));
|
||||
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}>
|
||||
@ -22,104 +28,115 @@ const Advanced = ({ resource, onChange }: ResourcePickerProps<string | AzureMetr
|
||||
isOpen={isAdvancedOpen}
|
||||
onToggle={() => setIsAdvancedOpen(!isAdvancedOpen)}
|
||||
>
|
||||
{typeof resource === 'string' ? (
|
||||
<>
|
||||
{' '}
|
||||
<Label htmlFor="input-advanced-resource-picker">
|
||||
<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}
|
||||
{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}
|
||||
>
|
||||
<Icon name="info-circle" />
|
||||
</Tooltip>
|
||||
</h6>
|
||||
</Label>
|
||||
<Input
|
||||
id="input-advanced-resource-picker"
|
||||
value={resource}
|
||||
onChange={(event) => onChange(event.currentTarget.value)}
|
||||
placeholder="ex: /subscriptions/$subId"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<InlineField
|
||||
label="Subscription"
|
||||
grow
|
||||
transparent
|
||||
htmlFor="input-advanced-resource-picker-subscription"
|
||||
labelWidth={15}
|
||||
data-testid={selectors.components.queryEditor.resourcePicker.advanced.subscription.input}
|
||||
>
|
||||
<Input
|
||||
id="input-advanced-resource-picker-subscription"
|
||||
value={resource?.subscription ?? ''}
|
||||
onChange={(event) => onChange({ ...resource, subscription: event.currentTarget.value })}
|
||||
placeholder="aaaaaaaa-bbbb-cccc-dddd-eeeeeeee"
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="Resource Group"
|
||||
grow
|
||||
transparent
|
||||
htmlFor="input-advanced-resource-picker-resourceGroup"
|
||||
labelWidth={15}
|
||||
data-testid={selectors.components.queryEditor.resourcePicker.advanced.resourceGroup.input}
|
||||
>
|
||||
<Input
|
||||
id="input-advanced-resource-picker-resourceGroup"
|
||||
value={resource?.resourceGroup ?? ''}
|
||||
onChange={(event) => onChange({ ...resource, resourceGroup: event.currentTarget.value })}
|
||||
placeholder="resource-group"
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="Namespace"
|
||||
grow
|
||||
transparent
|
||||
htmlFor="input-advanced-resource-picker-metricNamespace"
|
||||
labelWidth={15}
|
||||
data-testid={selectors.components.queryEditor.resourcePicker.advanced.namespace.input}
|
||||
>
|
||||
<Input
|
||||
id="input-advanced-resource-picker-metricNamespace"
|
||||
value={resource?.metricNamespace ?? ''}
|
||||
onChange={(event) => onChange({ ...resource, metricNamespace: event.currentTarget.value })}
|
||||
placeholder="Microsoft.Insights/metricNamespaces"
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
label="Resource Name"
|
||||
grow
|
||||
transparent
|
||||
htmlFor="input-advanced-resource-picker-resourceName"
|
||||
labelWidth={15}
|
||||
data-testid={selectors.components.queryEditor.resourcePicker.advanced.resource.input}
|
||||
>
|
||||
<Input
|
||||
id="input-advanced-resource-picker-resourceName"
|
||||
value={resource?.resourceName ?? ''}
|
||||
onChange={(event) => onChange({ ...resource, resourceName: event.currentTarget.value })}
|
||||
placeholder="name"
|
||||
/>
|
||||
</InlineField>
|
||||
</>
|
||||
)}
|
||||
<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>
|
||||
|
@ -17,6 +17,15 @@ import { ResourceRowType } from './types';
|
||||
|
||||
import ResourcePicker from '.';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getTemplateSrv: () => ({
|
||||
replace: (val: string) => {
|
||||
return val;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const noResourceURI = '';
|
||||
const singleSubscriptionSelectionURI = '/subscriptions/def-456';
|
||||
const singleResourceGroupSelectionURI = '/subscriptions/def-456/resourceGroups/dev-3';
|
||||
@ -50,7 +59,7 @@ const queryType: ResourcePickerQueryType = 'logs';
|
||||
|
||||
const defaultProps = {
|
||||
templateVariables: [],
|
||||
resource: noResourceURI,
|
||||
resources: [noResourceURI],
|
||||
resourcePickerData: createMockResourcePickerData(),
|
||||
onCancel: noop,
|
||||
onApply: noop,
|
||||
@ -68,7 +77,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
});
|
||||
it('should pre-load subscriptions when there is no existing selection', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={noResourceURI} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[noResourceURI]} />);
|
||||
const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
|
||||
expect(subscriptionCheckbox).toBeInTheDocument();
|
||||
expect(subscriptionCheckbox).not.toBeChecked();
|
||||
@ -77,7 +86,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
});
|
||||
|
||||
it('should show a subscription as selected if there is one saved', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={singleSubscriptionSelectionURI} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[singleSubscriptionSelectionURI]} />);
|
||||
const subscriptionCheckboxes = await screen.findAllByLabelText('Dev Subscription');
|
||||
expect(subscriptionCheckboxes.length).toBe(2);
|
||||
expect(subscriptionCheckboxes[0]).toBeChecked();
|
||||
@ -85,7 +94,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
});
|
||||
|
||||
it('should show a resourceGroup as selected if there is one saved', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={singleResourceGroupSelectionURI} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[singleResourceGroupSelectionURI]} />);
|
||||
const resourceGroupCheckboxes = await screen.findAllByLabelText('A Great Resource Group');
|
||||
expect(resourceGroupCheckboxes.length).toBe(2);
|
||||
expect(resourceGroupCheckboxes[0]).toBeChecked();
|
||||
@ -93,7 +102,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
});
|
||||
|
||||
it('should show scroll down to a resource and mark it as selected if there is one saved', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={singleResourceSelectionURI} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[singleResourceSelectionURI]} />);
|
||||
const resourceCheckboxes = await screen.findAllByLabelText('db-server');
|
||||
expect(resourceCheckboxes.length).toBe(2);
|
||||
expect(resourceCheckboxes[0]).toBeChecked();
|
||||
@ -101,7 +110,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
});
|
||||
|
||||
it('opens the selected nested resources', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={singleResourceSelectionURI} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[singleResourceSelectionURI]} />);
|
||||
const collapseSubscriptionBtn = await screen.findByLabelText('Collapse Dev Subscription');
|
||||
expect(collapseSubscriptionBtn).toBeInTheDocument();
|
||||
const collapseResourceGroupBtn = await screen.findByLabelText('Collapse A Great Resource Group');
|
||||
@ -109,7 +118,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
});
|
||||
|
||||
it('scrolls down to the selected resource', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={singleResourceSelectionURI} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[singleResourceSelectionURI]} />);
|
||||
await screen.findByLabelText('Collapse A Great Resource Group');
|
||||
expect(window.HTMLElement.prototype.scrollIntoView).toBeCalledTimes(1);
|
||||
});
|
||||
@ -133,12 +142,40 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
const applyButton = screen.getByRole('button', { name: 'Apply' });
|
||||
applyButton.click();
|
||||
expect(onApply).toBeCalledTimes(1);
|
||||
expect(onApply).toBeCalledWith('/subscriptions/def-123');
|
||||
expect(onApply).toBeCalledWith(['/subscriptions/def-123']);
|
||||
});
|
||||
|
||||
it('should call onApply removing an element', async () => {
|
||||
const onApply = jest.fn();
|
||||
render(<ResourcePicker {...defaultProps} resources={['/subscriptions/def-123']} onApply={onApply} />);
|
||||
const subscriptionCheckbox = await screen.findAllByLabelText('Primary Subscription');
|
||||
expect(subscriptionCheckbox).toHaveLength(2);
|
||||
expect(subscriptionCheckbox.at(0)).toBeChecked();
|
||||
subscriptionCheckbox.at(0)?.click();
|
||||
const applyButton = screen.getByRole('button', { name: 'Apply' });
|
||||
applyButton.click();
|
||||
expect(onApply).toBeCalledTimes(1);
|
||||
expect(onApply).toBeCalledWith([]);
|
||||
});
|
||||
|
||||
it('should call onApply removing an element ignoring the case', async () => {
|
||||
const onApply = jest.fn();
|
||||
render(
|
||||
<ResourcePicker {...defaultProps} resources={['/subscriptions/def-456/resourceGroups/DEV-3']} onApply={onApply} />
|
||||
);
|
||||
const subscriptionCheckbox = await screen.findAllByLabelText('A Great Resource Group');
|
||||
expect(subscriptionCheckbox).toHaveLength(2);
|
||||
expect(subscriptionCheckbox.at(0)).toBeChecked();
|
||||
subscriptionCheckbox.at(0)?.click();
|
||||
const applyButton = screen.getByRole('button', { name: 'Apply' });
|
||||
applyButton.click();
|
||||
expect(onApply).toBeCalledTimes(1);
|
||||
expect(onApply).toBeCalledWith([]);
|
||||
});
|
||||
|
||||
it('should call onApply with a new subscription when a user clicks on the checkbox in the row', async () => {
|
||||
const onApply = jest.fn();
|
||||
render(<ResourcePicker {...defaultProps} onApply={onApply} resource={{}} />);
|
||||
render(<ResourcePicker {...defaultProps} onApply={onApply} resources={[]} />);
|
||||
const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
|
||||
expect(subscriptionCheckbox).toBeInTheDocument();
|
||||
expect(subscriptionCheckbox).not.toBeChecked();
|
||||
@ -146,7 +183,20 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
const applyButton = screen.getByRole('button', { name: 'Apply' });
|
||||
applyButton.click();
|
||||
expect(onApply).toBeCalledTimes(1);
|
||||
expect(onApply).toBeCalledWith({ subscription: 'def-123' });
|
||||
expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]);
|
||||
});
|
||||
|
||||
it('should call onApply removing a resource element', async () => {
|
||||
const onApply = jest.fn();
|
||||
render(<ResourcePicker {...defaultProps} onApply={onApply} resources={[{ subscription: 'def-123' }]} />);
|
||||
const subscriptionCheckbox = await screen.findAllByLabelText('Primary Subscription');
|
||||
expect(subscriptionCheckbox).toHaveLength(2);
|
||||
expect(subscriptionCheckbox.at(0)).toBeChecked();
|
||||
subscriptionCheckbox.at(0)?.click();
|
||||
const applyButton = screen.getByRole('button', { name: 'Apply' });
|
||||
applyButton.click();
|
||||
expect(onApply).toBeCalledTimes(1);
|
||||
expect(onApply).toBeCalledWith([]);
|
||||
});
|
||||
|
||||
it('should call onApply with a new subscription uri when a user types it in the selection box', async () => {
|
||||
@ -166,12 +216,12 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
applyButton.click();
|
||||
|
||||
expect(onApply).toBeCalledTimes(1);
|
||||
expect(onApply).toBeCalledWith('/subscriptions/def-123');
|
||||
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} onApply={onApply} resource={{}} />);
|
||||
render(<ResourcePicker {...defaultProps} onApply={onApply} resources={[{}]} />);
|
||||
const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
|
||||
expect(subscriptionCheckbox).toBeInTheDocument();
|
||||
expect(subscriptionCheckbox).not.toBeChecked();
|
||||
@ -186,11 +236,11 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
applyButton.click();
|
||||
|
||||
expect(onApply).toBeCalledTimes(1);
|
||||
expect(onApply).toBeCalledWith({ subscription: 'def-123' });
|
||||
expect(onApply).toBeCalledWith([{ subscription: 'def-123' }]);
|
||||
});
|
||||
|
||||
it('should show unselect a subscription if the value is manually edited', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={{ subscription: 'def-456' }} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[{ subscription: 'def-456' }]} />);
|
||||
const subscriptionCheckboxes = await screen.findAllByLabelText('Dev Subscription');
|
||||
expect(subscriptionCheckboxes.length).toBe(2);
|
||||
expect(subscriptionCheckboxes[0]).toBeChecked();
|
||||
@ -264,7 +314,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
});
|
||||
|
||||
it('resets result when the user clears their search', async () => {
|
||||
render(<ResourcePicker {...defaultProps} resource={noResourceURI} />);
|
||||
render(<ResourcePicker {...defaultProps} resources={[noResourceURI]} />);
|
||||
const subscriptionCheckboxBeforeSearch = await screen.findByLabelText('Primary Subscription');
|
||||
expect(subscriptionCheckboxBeforeSearch).toBeInTheDocument();
|
||||
|
||||
@ -295,7 +345,7 @@ describe('AzureMonitor ResourcePicker', () => {
|
||||
{...defaultProps}
|
||||
queryType={'metrics'}
|
||||
resourcePickerData={resourcePickerData}
|
||||
resource={noResourceURI}
|
||||
resources={[noResourceURI]}
|
||||
/>
|
||||
);
|
||||
const subscriptionExpand = await screen.findByLabelText('Expand Primary Subscription');
|
||||
|
@ -15,21 +15,21 @@ import NestedRow from './NestedRow';
|
||||
import Search from './Search';
|
||||
import getStyles from './styles';
|
||||
import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types';
|
||||
import { findRow, parseResourceDetails, resourceToString } from './utils';
|
||||
import { findRows, parseMultipleResourceDetails, resourcesToStrings, matchURI, resourceToString } from './utils';
|
||||
|
||||
interface ResourcePickerProps<T> {
|
||||
resourcePickerData: ResourcePickerData;
|
||||
resource: T;
|
||||
resources: T[];
|
||||
selectableEntryTypes: ResourceRowType[];
|
||||
queryType: ResourcePickerQueryType;
|
||||
|
||||
onApply: (resource?: T) => void;
|
||||
onApply: (resources: T[]) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const ResourcePicker = ({
|
||||
resourcePickerData,
|
||||
resource,
|
||||
resources,
|
||||
onApply,
|
||||
onCancel,
|
||||
selectableEntryTypes,
|
||||
@ -40,14 +40,14 @@ const ResourcePicker = ({
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [rows, setRows] = useState<ResourceRowGroup>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<ResourceRowGroup>([]);
|
||||
const [internalSelected, setInternalSelected] = useState(resource);
|
||||
const [internalSelected, setInternalSelected] = useState(resources);
|
||||
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
|
||||
const [shouldShowLimitFlag, setShouldShowLimitFlag] = useState(false);
|
||||
|
||||
// Sync the resourceURI prop to internal state
|
||||
useEffect(() => {
|
||||
setInternalSelected(resource);
|
||||
}, [resource]);
|
||||
setInternalSelected(resources);
|
||||
}, [resources]);
|
||||
|
||||
const loadInitialData = useCallback(async () => {
|
||||
if (!isLoading) {
|
||||
@ -55,7 +55,7 @@ const ResourcePicker = ({
|
||||
setIsLoading(true);
|
||||
const resources = await resourcePickerData.fetchInitialRows(
|
||||
queryType,
|
||||
parseResourceDetails(internalSelected ?? {})
|
||||
parseMultipleResourceDetails(internalSelected ?? {})
|
||||
);
|
||||
setRows(resources);
|
||||
} catch (error) {
|
||||
@ -75,14 +75,9 @@ const ResourcePicker = ({
|
||||
setSelectedRows([]);
|
||||
}
|
||||
|
||||
const found = internalSelected && findRow(rows, resourceToString(internalSelected));
|
||||
if (found) {
|
||||
return setSelectedRows([
|
||||
{
|
||||
...found,
|
||||
children: undefined,
|
||||
},
|
||||
]);
|
||||
const found = internalSelected && findRows(rows, resourcesToStrings(internalSelected));
|
||||
if (found && found.length) {
|
||||
return setSelectedRows(found);
|
||||
}
|
||||
return setSelectedRows([]);
|
||||
}, [internalSelected, rows]);
|
||||
@ -109,19 +104,29 @@ const ResourcePicker = ({
|
||||
[resourcePickerData, rows, queryType]
|
||||
);
|
||||
|
||||
const resourceIsString = typeof resource === 'string';
|
||||
const resourceIsString = resources?.length && typeof resources[0] === 'string';
|
||||
const handleSelectionChanged = useCallback(
|
||||
(row: ResourceRow, isSelected: boolean) => {
|
||||
isSelected
|
||||
? setInternalSelected(resourceIsString ? row.uri : parseResourceDetails(row.uri, row.location))
|
||||
: setInternalSelected(resourceIsString ? '' : {});
|
||||
if (isSelected) {
|
||||
const newRes = resourceIsString ? row.uri : parseMultipleResourceDetails([row.uri], row.location)[0];
|
||||
const newSelected = (internalSelected ? internalSelected.concat(newRes) : [newRes]).filter((r) => {
|
||||
// avoid setting empty resources
|
||||
return typeof r === 'string' ? r !== '' : r.subscription;
|
||||
});
|
||||
setInternalSelected(newSelected);
|
||||
} else {
|
||||
const newInternalSelected = internalSelected?.filter((r) => {
|
||||
return !matchURI(resourceToString(r), row.uri);
|
||||
});
|
||||
setInternalSelected(newInternalSelected);
|
||||
}
|
||||
},
|
||||
[resourceIsString]
|
||||
[resourceIsString, internalSelected, setInternalSelected]
|
||||
);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
if (internalSelected) {
|
||||
onApply(resourceIsString ? internalSelected : parseResourceDetails(internalSelected));
|
||||
onApply(resourceIsString ? internalSelected : parseMultipleResourceDetails(internalSelected));
|
||||
}
|
||||
}, [resourceIsString, internalSelected, onApply]);
|
||||
|
||||
@ -230,7 +235,7 @@ const ResourcePicker = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<Advanced resource={internalSelected} onChange={(r) => setInternalSelected(r)} />
|
||||
<Advanced resources={internalSelected} onChange={(r) => setInternalSelected(r)} />
|
||||
<Space v={2} />
|
||||
|
||||
<Button
|
||||
|
@ -1,7 +1,24 @@
|
||||
import createMockQuery from '../../__mocks__/query';
|
||||
|
||||
import { ResourceRowGroup, ResourceRowType } from './types';
|
||||
import { findRow, parseResourceURI, setResource } from './utils';
|
||||
import {
|
||||
findRow,
|
||||
findRows,
|
||||
parseMultipleResourceDetails,
|
||||
parseResourceDetails,
|
||||
parseResourceURI,
|
||||
resourcesToStrings,
|
||||
setResource,
|
||||
} from './utils';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getTemplateSrv: () => ({
|
||||
replace: (val: string) => {
|
||||
return val;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('AzureMonitor ResourcePicker utils', () => {
|
||||
describe('parseResourceURI', () => {
|
||||
@ -163,6 +180,17 @@ describe('AzureMonitor ResourcePicker utils', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('findRows', () => {
|
||||
it('should find multiple rows', () => {
|
||||
const rows: ResourceRowGroup = [
|
||||
{ id: 'sub1', uri: '/subscriptions/sub1', name: '', type: ResourceRowType.Subscription, typeLabel: '' },
|
||||
{ id: 'sub2', uri: '/subscriptions/sub2', name: '', type: ResourceRowType.Subscription, typeLabel: '' },
|
||||
{ id: 'sub3', uri: '/subscriptions/sub3', name: '', type: ResourceRowType.Subscription, typeLabel: '' },
|
||||
];
|
||||
expect(findRows(rows, ['/subscriptions/sub1', '/subscriptions/sub2'])).toEqual([rows[0], rows[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setResource', () => {
|
||||
it('updates a resource with a resource URI for Log Analytics', () => {
|
||||
expect(setResource(createMockQuery(), '/subscription/sub')).toMatchObject({
|
||||
@ -196,4 +224,68 @@ describe('AzureMonitor ResourcePicker utils', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseResourceDetails', () => {
|
||||
it('parses a string resource', () => {
|
||||
expect(
|
||||
parseResourceDetails(
|
||||
'/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.Sql/servers/foo/databases/bar',
|
||||
'useast'
|
||||
)
|
||||
).toEqual({
|
||||
metricNamespace: 'Microsoft.Sql/servers/databases',
|
||||
resourceGroup: 'cloud-datasources',
|
||||
resourceName: 'foo/bar',
|
||||
subscription: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
region: 'useast',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseMultipleResourceDetails', () => {
|
||||
it('parses multiple string resources', () => {
|
||||
expect(
|
||||
parseMultipleResourceDetails(
|
||||
[
|
||||
'/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.Sql/servers/foo/databases/bar',
|
||||
'/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.Sql/servers/other/databases/resource',
|
||||
],
|
||||
'useast'
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
metricNamespace: 'Microsoft.Sql/servers/databases',
|
||||
resourceGroup: 'cloud-datasources',
|
||||
resourceName: 'foo/bar',
|
||||
subscription: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
region: 'useast',
|
||||
},
|
||||
{
|
||||
metricNamespace: 'Microsoft.Sql/servers/databases',
|
||||
resourceGroup: 'cloud-datasources',
|
||||
resourceName: 'other/resource',
|
||||
subscription: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
region: 'useast',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resourcesToStrings', () => {
|
||||
it('converts a resource to a string', () => {
|
||||
expect(
|
||||
resourcesToStrings([
|
||||
{
|
||||
metricNamespace: 'Microsoft.Sql/servers/databases',
|
||||
resourceGroup: 'cloud-datasources',
|
||||
resourceName: 'foo/bar',
|
||||
subscription: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee',
|
||||
region: 'useast',
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
'/subscriptions/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/resourceGroups/cloud-datasources/providers/Microsoft.Sql/servers/foo/databases/bar',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -43,6 +43,12 @@ export function parseResourceURI(resourceURI: string): AzureMetricResource {
|
||||
return { subscription, resourceGroup, metricNamespace, resourceName };
|
||||
}
|
||||
|
||||
export function parseMultipleResourceDetails(resources: Array<string | AzureMetricResource>, location?: string) {
|
||||
return resources.map((resource) => {
|
||||
return parseResourceDetails(resource, location);
|
||||
});
|
||||
}
|
||||
|
||||
export function parseResourceDetails(resource: string | AzureMetricResource, location?: string) {
|
||||
if (typeof resource === 'string') {
|
||||
const res = parseResourceURI(resource);
|
||||
@ -54,6 +60,10 @@ export function parseResourceDetails(resource: string | AzureMetricResource, loc
|
||||
return resource;
|
||||
}
|
||||
|
||||
export function resourcesToStrings(resources: Array<string | AzureMetricResource>) {
|
||||
return resources.map((resource) => resourceToString(resource));
|
||||
}
|
||||
|
||||
export function resourceToString(resource?: string | AzureMetricResource) {
|
||||
return resource
|
||||
? typeof resource === 'string'
|
||||
@ -82,7 +92,7 @@ function compareNamespaceAndName(
|
||||
return rowNamespace === resourceNamespace && rowName === resourceName;
|
||||
}
|
||||
|
||||
function matchURI(rowURI: string, resourceURI: string) {
|
||||
export function matchURI(rowURI: string, resourceURI: string) {
|
||||
const targetParams = parseResourceDetails(resourceURI);
|
||||
const rowParams = parseResourceDetails(rowURI);
|
||||
|
||||
@ -98,6 +108,17 @@ function matchURI(rowURI: string, resourceURI: string) {
|
||||
);
|
||||
}
|
||||
|
||||
export function findRows(rows: ResourceRowGroup, uris: string[]): ResourceRow[] {
|
||||
const result: ResourceRow[] = [];
|
||||
uris.forEach((uri) => {
|
||||
const row = findRow(rows, uri);
|
||||
if (row) {
|
||||
result.push(row);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function findRow(rows: ResourceRowGroup, uri: string): ResourceRow | undefined {
|
||||
for (const row of rows) {
|
||||
if (matchURI(row.uri, uri)) {
|
||||
|
@ -10,6 +10,15 @@ import { AzureGraphResponse } from '../types';
|
||||
|
||||
import ResourcePickerData from './resourcePickerData';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...(jest.requireActual('@grafana/runtime') as unknown as object),
|
||||
getTemplateSrv: () => ({
|
||||
replace: (val: string) => {
|
||||
return val;
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
const createResourcePickerData = (responses: AzureGraphResponse[]) => {
|
||||
const instanceSettings = createMockInstanceSetttings();
|
||||
const mockDatasource = createMockDatasource();
|
||||
@ -412,4 +421,52 @@ describe('AzureMonitor resourcePickerData', () => {
|
||||
expect(locations.get('northeurope')?.supportsLogs).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchInitialRows', () => {
|
||||
it('returns a list of subscriptions', async () => {
|
||||
const { resourcePickerData } = createResourcePickerData([createMockARGSubscriptionResponse()]);
|
||||
const rows = await resourcePickerData.fetchInitialRows('logs');
|
||||
expect(rows.length).toEqual(createMockARGSubscriptionResponse().data.length);
|
||||
});
|
||||
|
||||
it('fetches resource groups and resources', async () => {
|
||||
const { resourcePickerData } = createResourcePickerData([createMockARGSubscriptionResponse()]);
|
||||
resourcePickerData.getResourceGroupsBySubscriptionId = jest
|
||||
.fn()
|
||||
.mockResolvedValue([{ id: 'rg1', uri: '/subscriptions/1/resourceGroups/rg1' }]);
|
||||
resourcePickerData.getResourcesForResourceGroup = jest.fn().mockResolvedValue([
|
||||
{ id: 'vm1', uri: '/subscriptions/1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm1' },
|
||||
{ id: 'vm2', uri: '/subscriptions/1/resourceGroups/rg1/providers/Microsoft.Compute/virtualMachines/vm2' },
|
||||
]);
|
||||
const rows = await resourcePickerData.fetchInitialRows('logs', [
|
||||
{
|
||||
subscription: '1',
|
||||
resourceGroup: 'rg1',
|
||||
resourceName: 'vm1',
|
||||
metricNamespace: 'Microsoft.Compute/virtualMachines',
|
||||
},
|
||||
{
|
||||
subscription: '1',
|
||||
resourceGroup: 'rg1',
|
||||
resourceName: 'vm2',
|
||||
metricNamespace: 'Microsoft.Compute/virtualMachines',
|
||||
},
|
||||
]);
|
||||
expect(rows[0]).toMatchObject({
|
||||
id: '1',
|
||||
children: [
|
||||
{
|
||||
id: 'rg1',
|
||||
children: [{ id: 'vm1' }, { id: 'vm2' }],
|
||||
},
|
||||
],
|
||||
});
|
||||
// getResourceGroupsBySubscriptionId should only be called once because the subscription
|
||||
// of both resources is the same
|
||||
expect(resourcePickerData.getResourceGroupsBySubscriptionId).toBeCalledTimes(1);
|
||||
// getResourcesForResourceGroup should only be called once because the resource group
|
||||
// of both resources is the same
|
||||
expect(resourcePickerData.getResourcesForResourceGroup).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -6,7 +6,13 @@ import { DataSourceWithBackend } from '@grafana/runtime';
|
||||
import { logsResourceTypes, resourceTypeDisplayNames } from '../azureMetadata';
|
||||
import AzureMonitorDatasource from '../azure_monitor/azure_monitor_datasource';
|
||||
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
|
||||
import { addResources, parseResourceDetails, parseResourceURI } from '../components/ResourcePicker/utils';
|
||||
import {
|
||||
addResources,
|
||||
findRow,
|
||||
parseResourceDetails,
|
||||
parseResourceURI,
|
||||
resourceToString,
|
||||
} from '../components/ResourcePicker/utils';
|
||||
import {
|
||||
AzureDataSourceJsonData,
|
||||
AzureGraphResponse,
|
||||
@ -46,7 +52,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
|
||||
|
||||
async fetchInitialRows(
|
||||
type: ResourcePickerQueryType,
|
||||
currentSelection?: AzureMetricResource
|
||||
currentSelection?: AzureMetricResource[]
|
||||
): Promise<ResourceRowGroup> {
|
||||
const subscriptions = await this.getSubscriptions();
|
||||
|
||||
@ -60,19 +66,29 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
|
||||
}
|
||||
|
||||
let resources = subscriptions;
|
||||
if (currentSelection.subscription) {
|
||||
const resourceGroupURI = `/subscriptions/${currentSelection.subscription}/resourceGroups/${currentSelection.resourceGroup}`;
|
||||
const promises = currentSelection.map((selection) => async () => {
|
||||
if (selection.subscription) {
|
||||
const resourceGroupURI = `/subscriptions/${selection.subscription}/resourceGroups/${selection.resourceGroup}`;
|
||||
|
||||
if (currentSelection.resourceGroup) {
|
||||
const resourceGroups = await this.getResourceGroupsBySubscriptionId(currentSelection.subscription, type);
|
||||
resources = addResources(resources, `/subscriptions/${currentSelection.subscription}`, resourceGroups);
|
||||
}
|
||||
if (selection.resourceGroup && !findRow(resources, resourceGroupURI)) {
|
||||
const resourceGroups = await this.getResourceGroupsBySubscriptionId(selection.subscription, type);
|
||||
resources = addResources(resources, `/subscriptions/${selection.subscription}`, resourceGroups);
|
||||
}
|
||||
|
||||
if (currentSelection.resourceName) {
|
||||
const resourcesForResourceGroup = await this.getResourcesForResourceGroup(resourceGroupURI, type);
|
||||
resources = addResources(resources, resourceGroupURI, resourcesForResourceGroup);
|
||||
const resourceURI = resourceToString(selection);
|
||||
if (selection.resourceName && !findRow(resources, resourceURI)) {
|
||||
const resourcesForResourceGroup = await this.getResourcesForResourceGroup(resourceGroupURI, type);
|
||||
resources = addResources(resources, resourceGroupURI, resourcesForResourceGroup);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (const promise of promises) {
|
||||
// Fetch resources one by one, avoiding re-fetching the same resource
|
||||
// and race conditions updating the resources array
|
||||
await promise();
|
||||
}
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user