Azure Monitor: Small bug fixes for Resource Picker (#46665)

- fixes issue with checkbox styling
- fixes issue with selecting subscriptions
This commit is contained in:
Sarah Zinger 2022-03-23 09:38:46 -04:00 committed by GitHub
parent 721c3207e9
commit dd49f9d182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 448 additions and 490 deletions

View File

@ -41,7 +41,6 @@ export default function createMockDatasource(overrides?: DeepPartial<Datasource>
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]), getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]),
getResourcesForResourceGroup: jest.fn().mockResolvedValue([]), getResourcesForResourceGroup: jest.fn().mockResolvedValue([]),
getResourceURIFromWorkspace: jest.fn().mockReturnValue(''), getResourceURIFromWorkspace: jest.fn().mockReturnValue(''),
transformVariablesToRow: jest.fn().mockReturnValue({}),
}, },
...overrides, ...overrides,
}; };

View File

@ -10,7 +10,6 @@ export default function createMockResourcePickerData(overrides?: DeepPartial<Res
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]), getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue([]),
getResourcesForResourceGroup: jest.fn().mockResolvedValue([]), getResourcesForResourceGroup: jest.fn().mockResolvedValue([]),
getResourceURIFromWorkspace: jest.fn().mockReturnValue(''), getResourceURIFromWorkspace: jest.fn().mockReturnValue(''),
transformVariablesToRow: jest.fn().mockReturnValue({}),
...overrides, ...overrides,
}; };

View File

@ -1,115 +1,9 @@
import { ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types'; import { ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
export const createMockResourcePickerRows = (): ResourceRowGroup => [
{
id: '/subscriptions/abc-123',
name: 'Primary Subscription',
type: ResourceRowType.Subscription,
typeLabel: 'Subscription',
children: [
{
id: '/subscriptions/abc-123/resourceGroups/prod',
name: 'Production',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
},
{
id: '/subscriptions/abc-123/resourceGroups/pre-prod',
name: 'Pre-production',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
},
],
},
{
id: '/subscriptions/def-456',
name: 'Dev Subscription',
type: ResourceRowType.Subscription,
typeLabel: 'Subscription',
children: [
{
id: '/subscriptions/def-456/resourceGroups/dev',
name: 'Development',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [
{
id: '/subscription/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/web-server',
name: 'web-server',
typeLabel: 'Microsoft.Compute/virtualMachines',
type: ResourceRowType.Resource,
location: 'northeurope',
},
{
id: '/subscription/def-456/resourceGroups/dev/providers/Microsoft.Compute/disks/web-server_DataDisk',
name: 'web-server_DataDisk',
typeLabel: 'Microsoft.Compute/disks',
type: ResourceRowType.Resource,
location: 'northeurope',
},
{
id: '/subscription/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/db-server',
name: 'db-server',
typeLabel: 'Microsoft.Compute/virtualMachines',
type: ResourceRowType.Resource,
location: 'northeurope',
},
{
id: '/subscription/def-456/resourceGroups/dev/providers/Microsoft.Compute/disks/db-server_DataDisk',
name: 'db-server_DataDisk',
typeLabel: 'Microsoft.Compute/disks',
type: ResourceRowType.Resource,
location: 'northeurope',
},
],
},
{
id: '/subscriptions/def-456/resourceGroups/test',
name: 'Test',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
},
{
id: '/subscriptions/def-456/resourceGroups/qa',
name: 'QA',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
},
],
},
{
id: '$$grafana-templateVariables$$',
name: 'Template variables',
type: ResourceRowType.VariableGroup,
typeLabel: 'Variables',
children: [
{
id: '$machine',
name: '$machine',
type: ResourceRowType.Variable,
typeLabel: 'Variable',
},
{
id: '$workspace',
name: '$workspace',
type: ResourceRowType.Variable,
typeLabel: 'Variable',
},
],
},
];
export const createMockSubscriptions = (): ResourceRowGroup => [ export const createMockSubscriptions = (): ResourceRowGroup => [
{ {
id: 'def-123', id: 'def-123',
uri: '/subscriptions/def-123',
name: 'Primary Subscription', name: 'Primary Subscription',
type: ResourceRowType.Subscription, type: ResourceRowType.Subscription,
typeLabel: 'Subscription', typeLabel: 'Subscription',
@ -117,6 +11,7 @@ export const createMockSubscriptions = (): ResourceRowGroup => [
}, },
{ {
id: 'def-456', id: 'def-456',
uri: '/subscriptions/def-456',
name: 'Dev Subscription', name: 'Dev Subscription',
type: ResourceRowType.Subscription, type: ResourceRowType.Subscription,
typeLabel: 'Subscription', typeLabel: 'Subscription',
@ -124,6 +19,7 @@ export const createMockSubscriptions = (): ResourceRowGroup => [
}, },
{ {
id: 'def-789', id: 'def-789',
uri: '/subscriptions/def-789',
name: 'Test Subscription', name: 'Test Subscription',
type: ResourceRowType.Subscription, type: ResourceRowType.Subscription,
typeLabel: 'Subscription', typeLabel: 'Subscription',
@ -133,35 +29,40 @@ export const createMockSubscriptions = (): ResourceRowGroup => [
export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
{ {
id: '/subscriptions/def-456/resourceGroups/dev-1', id: 'dev-1',
uri: '/subscriptions/def-456/resourceGroups/dev-1',
name: 'Development', name: 'Development',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
children: [], children: [],
}, },
{ {
id: '/subscriptions/def-456/resourceGroups/dev-2', id: 'dev-2',
uri: '/subscriptions/def-456/resourceGroups/dev-2',
name: 'Development', name: 'Development',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
children: [], children: [],
}, },
{ {
id: '/subscriptions/def-456/resourceGroups/dev-3', id: 'dev-3',
uri: '/subscriptions/def-456/resourceGroups/dev-3',
name: 'A Great Resource Group',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
},
{
id: 'dev-4',
uri: '/subscriptions/def-456/resourceGroups/dev-4',
name: 'Development', name: 'Development',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
children: [], children: [],
}, },
{ {
id: '/subscriptions/def-456/resourceGroups/dev-4', id: 'dev-5',
name: 'Development', uri: '/subscriptions/def-456/resourceGroups/dev-5',
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
},
{
id: '/subscriptions/def-456/resourceGroups/dev-5',
name: 'Development', name: 'Development',
type: ResourceRowType.ResourceGroup, type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group', typeLabel: 'Resource Group',
@ -171,14 +72,16 @@ export const createMockResourceGroupsBySubscription = (): ResourceRowGroup => [
export const mockResourcesByResourceGroup = (): ResourceRowGroup => [ export const mockResourcesByResourceGroup = (): ResourceRowGroup => [
{ {
id: 'Microsoft.Compute/virtualMachines/web-server', id: 'web-server',
uri: '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/web-server',
name: 'web-server', name: 'web-server',
typeLabel: 'Microsoft.Compute/virtualMachines', typeLabel: 'Microsoft.Compute/virtualMachines',
type: ResourceRowType.Resource, type: ResourceRowType.Resource,
location: 'northeurope', location: 'northeurope',
}, },
{ {
id: 'Microsoft.Compute/disks/web-server_DataDisk', id: 'web-server_DataDisk',
uri: '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/disks/web-server_DataDisk',
name: 'web-server_DataDisk', name: 'web-server_DataDisk',
typeLabel: 'Microsoft.Compute/disks', typeLabel: 'Microsoft.Compute/disks',
type: ResourceRowType.Resource, type: ResourceRowType.Resource,
@ -186,7 +89,8 @@ export const mockResourcesByResourceGroup = (): ResourceRowGroup => [
}, },
{ {
id: 'Microsoft.Compute/virtualMachines/db-server', id: 'db-server',
uri: '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server',
name: 'db-server', name: 'db-server',
typeLabel: 'Microsoft.Compute/virtualMachines', typeLabel: 'Microsoft.Compute/virtualMachines',
type: ResourceRowType.Resource, type: ResourceRowType.Resource,
@ -194,7 +98,8 @@ export const mockResourcesByResourceGroup = (): ResourceRowGroup => [
}, },
{ {
id: 'Microsoft.Compute/disks/db-server_DataDisk', id: 'db-server_DataDisk',
uri: '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/disks/db-server_DataDisk',
name: 'db-server_DataDisk', name: 'db-server_DataDisk',
typeLabel: 'Microsoft.Compute/disks', typeLabel: 'Microsoft.Compute/disks',
type: ResourceRowType.Resource, type: ResourceRowType.Resource,

View File

@ -1,108 +0,0 @@
import { act, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { createMockResourcePickerRows } from '../../__mocks__/resourcePickerRows';
import NestedResourceTable from './NestedResourceTable';
import { findRow } from './utils';
describe('AzureMonitor NestedResourceTable', () => {
const noop: any = () => {};
const getElementById = document.getElementById;
beforeEach(() => {
document.getElementById = jest.fn().mockReturnValue({
scrollIntoView: jest.fn(),
});
});
afterEach(() => {
document.getElementById = getElementById;
});
it('renders subscriptions', () => {
const rows = createMockResourcePickerRows();
render(<NestedResourceTable rows={rows} selectedRows={[]} requestNestedRows={noop} onRowSelectedChange={noop} />);
expect(screen.getByText('Primary Subscription')).toBeInTheDocument();
expect(screen.getByText('Dev Subscription')).toBeInTheDocument();
});
it('opens to the selected resource', () => {
const rows = createMockResourcePickerRows();
const selected = findRow(
rows,
'/subscription/def-456/resourceGroups/dev/providers/Microsoft.Compute/disks/web-server_DataDisk'
);
if (!selected) {
throw new Error("couldn't find row, test data stale");
}
render(
<NestedResourceTable rows={rows} selectedRows={[selected]} requestNestedRows={noop} onRowSelectedChange={noop} />
);
expect(screen.getByText('web-server_DataDisk')).toBeInTheDocument();
});
it("expands subscriptions when they're clicked", async () => {
const rows = createMockResourcePickerRows();
const promise = Promise.resolve();
const requestNestedRows = jest.fn().mockReturnValue(promise);
render(
<NestedResourceTable
rows={rows}
selectedRows={[]}
requestNestedRows={requestNestedRows}
onRowSelectedChange={noop}
/>
);
const expandButton = screen.getAllByLabelText('Expand')[1];
userEvent.click(expandButton);
expect(requestNestedRows).toBeCalledWith(
expect.objectContaining({
id: '/subscriptions/def-456',
name: 'Dev Subscription',
typeLabel: 'Subscription',
})
);
expect(screen.queryByText('Development')).not.toBeInTheDocument();
await act(() => promise);
expect(screen.getByText('Development')).toBeInTheDocument();
});
it('supports selecting variables', async () => {
const rows = createMockResourcePickerRows();
const promise = Promise.resolve();
const requestNestedRows = jest.fn().mockReturnValue(promise);
const onRowSelectedChange = jest.fn();
render(
<NestedResourceTable
rows={rows}
selectedRows={[]}
requestNestedRows={requestNestedRows}
onRowSelectedChange={onRowSelectedChange}
/>
);
const expandButton = screen.getAllByLabelText('Expand')[2];
userEvent.click(expandButton);
await act(() => promise);
const checkbox = screen.getByLabelText('$workspace');
userEvent.click(checkbox);
expect(onRowSelectedChange).toHaveBeenCalledWith(
expect.objectContaining({
id: '$workspace',
name: '$workspace',
}),
true
);
});
});

View File

@ -67,8 +67,7 @@ const NestedRow: React.FC<NestedRowProps> = ({ row, selectedRows, level, request
useEffect(() => { useEffect(() => {
// Assuming we don't have multi-select yet // Assuming we don't have multi-select yet
const selectedRow = selectedRows[0]; const selectedRow = selectedRows[0];
const containsChild = selectedRow && !!findRow(row.children ?? [], selectedRow.uri);
const containsChild = selectedRow && !!findRow(row.children ?? [], selectedRow.id);
if (containsChild) { if (containsChild) {
setRowStatus('open'); setRowStatus('open');
@ -203,7 +202,7 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
<IconButton <IconButton
className={styles.collapseButton} className={styles.collapseButton}
name={isOpen ? 'angle-down' : 'angle-right'} name={isOpen ? 'angle-down' : 'angle-right'}
aria-label={isOpen ? 'Collapse' : 'Expand'} aria-label={isOpen ? `Collapse ${entry.name}` : `Expand ${entry.name}`}
onClick={handleToggleCollapse} onClick={handleToggleCollapse}
id={entry.id} id={entry.id}
/> />
@ -215,7 +214,13 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
{isSelectable && ( {isSelectable && (
<> <>
<Checkbox id={checkboxId} onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} /> <Checkbox
id={checkboxId}
onChange={handleSelectedChanged}
disabled={isDisabled}
value={isSelected}
className={styles.nestedRowCheckbox}
/>
<Space layout="inline" h={2} /> <Space layout="inline" h={2} />
</> </>
)} )}

View File

@ -10,119 +10,143 @@ import {
} from '../../__mocks__/resourcePickerRows'; } from '../../__mocks__/resourcePickerRows';
const noResourceURI = ''; const noResourceURI = '';
const singleSubscriptionSelectionURI = 'def-456'; const singleSubscriptionSelectionURI = '/subscriptions/def-456';
const singleResourceGroupSelectionURI = '/subscriptions/def-456/resourceGroups/dev-3'; const singleResourceGroupSelectionURI = '/subscriptions/def-456/resourceGroups/dev-3';
const singleResourceSelectionURI = const singleResourceSelectionURI =
'/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-serverproviders/Microsoft.Compute/virtualMachines/db-server'; '/subscriptions/def-456/resourceGroups/dev-3/providers/Microsoft.Compute/virtualMachines/db-server';
const createResourcePickerDataMock = () => {
return createMockResourcePickerData({
getSubscriptions: jest.fn().mockResolvedValue(createMockSubscriptions()),
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue(createMockResourceGroupsBySubscription()),
getResourcesForResourceGroup: jest.fn().mockResolvedValue(mockResourcesByResourceGroup()),
});
};
describe('AzureMonitor ResourcePicker', () => { describe('AzureMonitor ResourcePicker', () => {
const noop: any = () => {}; const noop: any = () => {};
beforeEach(() => { beforeEach(() => {
window.HTMLElement.prototype.scrollIntoView = function () {}; window.HTMLElement.prototype.scrollIntoView = function () {};
}); });
describe('when rendering the resource picker without a selection', () => { it('should pre-load subscriptions when there is no existing selection', async () => {
it('should load subscriptions', async () => { render(
const resourePickerDataMock = createMockResourcePickerData({ <ResourcePicker
getSubscriptions: jest.fn().mockResolvedValue(createMockSubscriptions()), templateVariables={[]}
getResourceGroupsBySubscriptionId: jest.fn(), resourcePickerData={createResourcePickerDataMock()}
getResourcesForResourceGroup: jest.fn(), resourceURI={noResourceURI}
}); onCancel={noop}
render( onApply={noop}
<ResourcePicker />
templateVariables={[]} );
resourcePickerData={resourePickerDataMock} const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
resourceURI={noResourceURI} expect(subscriptionCheckbox).toBeInTheDocument();
onCancel={noop} expect(subscriptionCheckbox).not.toBeChecked();
onApply={noop} const uncheckedCheckboxes = await screen.findAllByRole('checkbox', { checked: false });
/> expect(uncheckedCheckboxes.length).toBe(3);
);
expect(await screen.findByText('Primary Subscription')).toBeInTheDocument();
expect(resourePickerDataMock.getSubscriptions).toHaveBeenCalledTimes(1);
expect(resourePickerDataMock.getResourceGroupsBySubscriptionId).not.toHaveBeenCalled();
expect(resourePickerDataMock.getResourcesForResourceGroup).not.toHaveBeenCalled();
});
}); });
describe('when rendering the resource picker with a subscription selected', () => { it('should show a subscription as selected if there is one saved', async () => {
it('should load subscriptions once', async () => { render(
const resourePickerDataMock = createMockResourcePickerData({ <ResourcePicker
getSubscriptions: jest.fn().mockResolvedValue(createMockSubscriptions()), templateVariables={[]}
getResourceGroupsBySubscriptionId: jest.fn(), resourcePickerData={createResourcePickerDataMock()}
getResourcesForResourceGroup: jest.fn(), resourceURI={singleSubscriptionSelectionURI}
}); onCancel={noop}
render( onApply={noop}
<ResourcePicker />
templateVariables={[]} );
resourcePickerData={resourePickerDataMock} const subscriptionCheckbox = await screen.findByLabelText('Dev Subscription');
resourceURI={singleSubscriptionSelectionURI} expect(subscriptionCheckbox).toBeChecked();
onCancel={noop}
onApply={noop}
/>
);
expect(await screen.findByText('Primary Subscription')).toBeInTheDocument();
expect(resourePickerDataMock.getSubscriptions).toHaveBeenCalledTimes(1);
expect(resourePickerDataMock.getResourceGroupsBySubscriptionId).not.toHaveBeenCalled();
expect(resourePickerDataMock.getResourcesForResourceGroup).not.toHaveBeenCalled();
});
}); });
describe('when rendering the resource picker with a resource group selected', () => { it('should show a resource group as selected if there is one saved', async () => {
it('should load subscriptions and resource groups for its parent subscription once', async () => { render(
const resourePickerDataMock = createMockResourcePickerData({ <ResourcePicker
getSubscriptions: jest.fn().mockResolvedValue(createMockSubscriptions()), templateVariables={[]}
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue(createMockResourceGroupsBySubscription()), resourcePickerData={createResourcePickerDataMock()}
getResourcesForResourceGroup: jest.fn(), resourceURI={singleResourceGroupSelectionURI}
}); onCancel={noop}
render( onApply={noop}
<ResourcePicker />
templateVariables={[]} );
resourcePickerData={resourePickerDataMock} const resourceGroupCheckbox = await screen.findByLabelText('A Great Resource Group');
resourceURI={singleResourceGroupSelectionURI} expect(resourceGroupCheckbox).toBeChecked();
onCancel={noop}
onApply={noop}
/>
);
expect(await screen.findByText('Primary Subscription')).toBeInTheDocument();
expect(resourePickerDataMock.getSubscriptions).toHaveBeenCalledTimes(1);
expect(resourePickerDataMock.getResourceGroupsBySubscriptionId).toHaveBeenCalledTimes(1);
expect(resourePickerDataMock.getResourceGroupsBySubscriptionId).toHaveBeenLastCalledWith(
singleSubscriptionSelectionURI
);
expect(resourePickerDataMock.getResourcesForResourceGroup).not.toHaveBeenCalled();
});
}); });
describe('when rendering the resource picker with a resource selected', () => { it('should show a resource as selected if there is one saved', async () => {
it('should load subscriptions, resource groups and resources once', async () => { render(
const resourePickerDataMock = createMockResourcePickerData({ <ResourcePicker
getSubscriptions: jest.fn().mockResolvedValue(createMockSubscriptions()), templateVariables={[]}
getResourceGroupsBySubscriptionId: jest.fn().mockResolvedValue(createMockResourceGroupsBySubscription()), resourcePickerData={createResourcePickerDataMock()}
getResourcesForResourceGroup: jest.fn().mockResolvedValue(mockResourcesByResourceGroup()), resourceURI={singleResourceSelectionURI}
}); onCancel={noop}
render( onApply={noop}
<ResourcePicker />
templateVariables={[]} );
resourcePickerData={resourePickerDataMock}
resourceURI={singleResourceSelectionURI}
onCancel={noop}
onApply={noop}
/>
);
expect(await screen.findByText('Primary Subscription')).toBeInTheDocument(); const resourceCheckbox = await screen.findByLabelText('db-server');
expect(resourePickerDataMock.getSubscriptions).toHaveBeenCalledTimes(1); expect(resourceCheckbox).toBeChecked();
expect(resourePickerDataMock.getResourceGroupsBySubscriptionId).toHaveBeenCalledTimes(1); });
expect(resourePickerDataMock.getResourceGroupsBySubscriptionId).toHaveBeenLastCalledWith(
singleSubscriptionSelectionURI it('should be able to expand a subscription when clicked and reveal resource groups', async () => {
); render(
expect(resourePickerDataMock.getResourcesForResourceGroup).toHaveBeenCalledTimes(1); <ResourcePicker
expect(resourePickerDataMock.getResourcesForResourceGroup).toHaveBeenLastCalledWith( templateVariables={[]}
singleResourceGroupSelectionURI resourcePickerData={createResourcePickerDataMock()}
); resourceURI={noResourceURI}
}); onCancel={noop}
onApply={noop}
/>
);
const expandSubscriptionButton = await screen.findByLabelText('Expand Primary Subscription');
expect(expandSubscriptionButton).toBeInTheDocument();
expect(screen.queryByLabelText('A Great Resource Group')).not.toBeInTheDocument();
expandSubscriptionButton.click();
expect(await screen.findByLabelText('A Great Resource Group')).toBeInTheDocument();
});
it('should call onApply with a new subscription uri when a user selects it', async () => {
const onApply = jest.fn();
render(
<ResourcePicker
templateVariables={[]}
resourcePickerData={createResourcePickerDataMock()}
resourceURI={noResourceURI}
onCancel={noop}
onApply={onApply}
/>
);
const subscriptionCheckbox = await screen.findByLabelText('Primary Subscription');
expect(subscriptionCheckbox).toBeInTheDocument();
expect(subscriptionCheckbox).not.toBeChecked();
subscriptionCheckbox.click();
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 template variable when a user selects it', async () => {
const onApply = jest.fn();
render(
<ResourcePicker
templateVariables={['$workspace']}
resourcePickerData={createResourcePickerDataMock()}
resourceURI={noResourceURI}
onCancel={noop}
onApply={onApply}
/>
);
const expandButton = await screen.findByLabelText('Expand Template variables');
expandButton.click();
const workSpaceCheckbox = await screen.findByLabelText('$workspace');
workSpaceCheckbox.click();
const applyButton = screen.getByRole('button', { name: 'Apply' });
applyButton.click();
expect(onApply).toBeCalledTimes(1);
expect(onApply).toBeCalledWith('$workspace');
}); });
}); });

View File

@ -10,6 +10,7 @@ import NestedResourceTable from './NestedResourceTable';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types'; import { ResourceRow, ResourceRowGroup, ResourceRowType } from './types';
import { addResources, findRow, parseResourceURI } from './utils'; import { addResources, findRow, parseResourceURI } from './utils';
const TEMPLATE_VARIABLE_GROUP_ID = '$$grafana-templateVariables$$';
interface ResourcePickerProps { interface ResourcePickerProps {
resourcePickerData: ResourcePickerData; resourcePickerData: ResourcePickerData;
resourceURI: string | undefined; resourceURI: string | undefined;
@ -31,12 +32,12 @@ const ResourcePicker = ({
type LoadingStatus = 'NotStarted' | 'Started' | 'Done'; type LoadingStatus = 'NotStarted' | 'Started' | 'Done';
const [loadingStatus, setLoadingStatus] = useState<LoadingStatus>('NotStarted'); const [loadingStatus, setLoadingStatus] = useState<LoadingStatus>('NotStarted');
const [azureRows, setAzureRows] = useState<ResourceRowGroup>([]); const [azureRows, setAzureRows] = useState<ResourceRowGroup>([]);
const [internalSelected, setInternalSelected] = useState<string | undefined>(resourceURI); const [internalSelectedURI, setInternalSelectedURI] = useState<string | undefined>(resourceURI);
const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined); const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined);
// Sync the resourceURI prop to internal state // Sync the resourceURI prop to internal state
useEffect(() => { useEffect(() => {
setInternalSelected(resourceURI); setInternalSelectedURI(resourceURI);
}, [resourceURI]); }, [resourceURI]);
// Request initial data on first mount // Request initial data on first mount
@ -46,13 +47,13 @@ const ResourcePicker = ({
try { try {
setLoadingStatus('Started'); setLoadingStatus('Started');
let resources = await resourcePickerData.getSubscriptions(); let resources = await resourcePickerData.getSubscriptions();
if (!internalSelected) { if (!internalSelectedURI) {
setAzureRows(resources); setAzureRows(resources);
setLoadingStatus('Done'); setLoadingStatus('Done');
return; return;
} }
const parsedURI = parseResourceURI(internalSelected ?? ''); const parsedURI = parseResourceURI(internalSelectedURI ?? '');
if (parsedURI) { if (parsedURI) {
const resourceGroupURI = `/subscriptions/${parsedURI.subscriptionID}/resourceGroups/${parsedURI.resourceGroup}`; const resourceGroupURI = `/subscriptions/${parsedURI.subscriptionID}/resourceGroups/${parsedURI.resourceGroup}`;
@ -61,7 +62,7 @@ const ResourcePicker = ({
const resourceGroups = await resourcePickerData.getResourceGroupsBySubscriptionId( const resourceGroups = await resourcePickerData.getResourceGroupsBySubscriptionId(
parsedURI.subscriptionID parsedURI.subscriptionID
); );
resources = addResources(resources, parsedURI.subscriptionID, resourceGroups); resources = addResources(resources, `/subscriptions/${parsedURI.subscriptionID}`, resourceGroups);
} }
// if a resource was previously selected, but the resources under the parent resource group have not been loaded yet // if a resource was previously selected, but the resources under the parent resource group have not been loaded yet
@ -80,16 +81,16 @@ const ResourcePicker = ({
loadInitialData(); loadInitialData();
} }
}, [resourcePickerData, internalSelected, azureRows, loadingStatus]); }, [resourcePickerData, internalSelectedURI, azureRows, loadingStatus]);
const rows = useMemo(() => { const rows = useMemo(() => {
const templateVariableRow = resourcePickerData.transformVariablesToRow(templateVariables); const templateVariableRow = transformVariablesToRow(templateVariables);
return templateVariables.length ? [...azureRows, templateVariableRow] : azureRows; return templateVariables.length ? [...azureRows, templateVariableRow] : azureRows;
}, [resourcePickerData, azureRows, templateVariables]); }, [azureRows, templateVariables]);
// Map the selected item into an array of rows // Map the selected item into an array of rows
const selectedResourceRows = useMemo(() => { const selectedResourceRows = useMemo(() => {
const found = internalSelected && findRow(rows, internalSelected); const found = internalSelectedURI && findRow(rows, internalSelectedURI);
return found return found
? [ ? [
{ {
@ -98,7 +99,7 @@ const ResourcePicker = ({
}, },
] ]
: []; : [];
}, [internalSelected, rows]); }, [internalSelectedURI, rows]);
// Request resources for a expanded resource group // Request resources for a expanded resource group
const requestNestedRows = useCallback( const requestNestedRows = useCallback(
@ -110,7 +111,7 @@ const ResourcePicker = ({
// template variable group, though that shouldn't happen in practice // template variable group, though that shouldn't happen in practice
if ( if (
resourceGroupOrSubscription.children?.length || resourceGroupOrSubscription.children?.length ||
resourceGroupOrSubscription.id === ResourcePickerData.templateVariableGroupID resourceGroupOrSubscription.uri === TEMPLATE_VARIABLE_GROUP_ID
) { ) {
return; return;
} }
@ -121,7 +122,7 @@ const ResourcePicker = ({
? await resourcePickerData.getResourceGroupsBySubscriptionId(resourceGroupOrSubscription.id) ? await resourcePickerData.getResourceGroupsBySubscriptionId(resourceGroupOrSubscription.id)
: await resourcePickerData.getResourcesForResourceGroup(resourceGroupOrSubscription.id); : await resourcePickerData.getResourcesForResourceGroup(resourceGroupOrSubscription.id);
const newRows = addResources(azureRows, resourceGroupOrSubscription.id, rows); const newRows = addResources(azureRows, resourceGroupOrSubscription.uri, rows);
setAzureRows(newRows); setAzureRows(newRows);
} catch (error) { } catch (error) {
@ -132,14 +133,13 @@ const ResourcePicker = ({
[resourcePickerData, azureRows] [resourcePickerData, azureRows]
); );
// Select
const handleSelectionChanged = useCallback((row: ResourceRow, isSelected: boolean) => { const handleSelectionChanged = useCallback((row: ResourceRow, isSelected: boolean) => {
isSelected ? setInternalSelected(row.id) : setInternalSelected(undefined); isSelected ? setInternalSelectedURI(row.uri) : setInternalSelectedURI(undefined);
}, []); }, []);
const handleApply = useCallback(() => { const handleApply = useCallback(() => {
onApply(internalSelected); onApply(internalSelectedURI);
}, [internalSelected, onApply]); }, [internalSelectedURI, onApply]);
return ( return (
<div> <div>
@ -211,3 +211,20 @@ const getStyles = (theme: GrafanaTheme2) => ({
color: theme.colors.text.secondary, color: theme.colors.text.secondary,
}), }),
}); });
function transformVariablesToRow(templateVariables: string[]): ResourceRow {
return {
id: TEMPLATE_VARIABLE_GROUP_ID,
uri: TEMPLATE_VARIABLE_GROUP_ID,
name: 'Template variables',
type: ResourceRowType.VariableGroup,
typeLabel: 'Variables',
children: templateVariables.map((v) => ({
id: v,
uri: v,
name: v,
type: ResourceRowType.Variable,
typeLabel: 'Variable',
})),
};
}

View File

@ -64,6 +64,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
}), }),
nestedRowCheckbox: css({
zIndex: 0,
}),
}); });
export default getStyles; export default getStyles;

View File

@ -7,7 +7,8 @@ export enum ResourceRowType {
} }
export interface ResourceRow { export interface ResourceRow {
id: string; id: string; // azure's raw data id usually passes along a uri (except in the case of subscriptions), to make things less confusing for ourselves we parse the id string out of the uri or vice versa
uri: string; // ex: /subscriptions/subid123
name: string; name: string;
type: ResourceRowType; type: ResourceRowType;
typeLabel: string; typeLabel: string;

View File

@ -26,14 +26,14 @@ export function isGUIDish(input: string) {
return !!input.match(/^[A-Z0-9]+/i); return !!input.match(/^[A-Z0-9]+/i);
} }
export function findRow(rows: ResourceRowGroup, id: string): ResourceRow | undefined { export function findRow(rows: ResourceRowGroup, uri: string): ResourceRow | undefined {
for (const row of rows) { for (const row of rows) {
if (row.id.toLowerCase() === id.toLowerCase()) { if (row.uri.toLowerCase() === uri.toLowerCase()) {
return row; return row;
} }
if (row.children) { if (row.children) {
const result = findRow(row.children, id); const result = findRow(row.children, uri);
if (result) { if (result) {
return result; return result;
@ -44,9 +44,9 @@ export function findRow(rows: ResourceRowGroup, id: string): ResourceRow | undef
return undefined; return undefined;
} }
export function addResources(rows: ResourceRowGroup, targetResourceGroupID: string, newResources: ResourceRowGroup) { export function addResources(rows: ResourceRowGroup, targetParentId: string, newResources: ResourceRowGroup) {
return produce(rows, (draftState) => { return produce(rows, (draftState) => {
const draftRow = findRow(draftState, targetResourceGroupID); const draftRow = findRow(draftState, targetParentId);
if (!draftRow) { if (!draftRow) {
// This case shouldn't happen often because we're usually coming here from a resource we already have // This case shouldn't happen often because we're usually coming here from a resource we already have

View File

@ -4,129 +4,246 @@ import {
createMockARGSubscriptionResponse, createMockARGSubscriptionResponse,
} from '../__mocks__/argResourcePickerResponse'; } from '../__mocks__/argResourcePickerResponse';
import { createMockInstanceSetttings } from '../__mocks__/instanceSettings'; import { createMockInstanceSetttings } from '../__mocks__/instanceSettings';
import { ResourceRowType } from '../components/ResourcePicker/types';
import ResourcePickerData from './resourcePickerData'; import ResourcePickerData from './resourcePickerData';
import { AzureGraphResponse } from '../types';
const instanceSettings = createMockInstanceSetttings(); const createResourcePickerData = (responses: AzureGraphResponse[]) => {
const resourcePickerData = new ResourcePickerData(instanceSettings); const instanceSettings = createMockInstanceSetttings();
let postResource: jest.Mock; const resourcePickerData = new ResourcePickerData(instanceSettings);
const postResource = jest.fn();
responses.forEach((res) => {
postResource.mockResolvedValueOnce(res);
});
resourcePickerData.postResource = postResource;
return { resourcePickerData, postResource };
};
describe('AzureMonitor resourcePickerData', () => { describe('AzureMonitor resourcePickerData', () => {
describe('getSubscriptions', () => { describe('getSubscriptions', () => {
beforeEach(() => { it('makes 1 call to ARG with the correct path and query arguments', async () => {
postResource = jest.fn().mockResolvedValue(createMockARGSubscriptionResponse()); const mockResponse = createMockARGSubscriptionResponse();
resourcePickerData.postResource = postResource; const { resourcePickerData, postResource } = createResourcePickerData([mockResponse]);
});
it('calls ARG API', async () => {
await resourcePickerData.getSubscriptions(); await resourcePickerData.getSubscriptions();
expect(postResource).toHaveBeenCalled(); expect(postResource).toBeCalledTimes(1);
const argQuery = postResource.mock.calls[0][1].query; const firstCall = postResource.mock.calls[0];
const [path, postBody] = firstCall;
expect(path).toEqual('resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01');
expect(postBody.query).toContain("where type == 'microsoft.resources/subscriptions'");
});
it('returns formatted subscriptions', async () => {
const mockResponse = createMockARGSubscriptionResponse();
const { resourcePickerData } = createResourcePickerData([mockResponse]);
expect(argQuery).toContain(`where type == 'microsoft.resources/subscriptions'`); const subscriptions = await resourcePickerData.getSubscriptions();
expect(subscriptions.length).toEqual(6);
expect(subscriptions[0]).toEqual({
id: '1',
name: 'Primary Subscription',
type: 'Subscription',
typeLabel: 'Subscription',
uri: '/subscriptions/1',
children: [],
});
}); });
describe('when there is more than one page', () => { it('makes multiple requests when arg returns a skipToken and passes the right skipToken to each subsequent call', async () => {
beforeEach(() => { const response1 = {
const response1 = { ...createMockARGSubscriptionResponse(),
...createMockARGSubscriptionResponse(), $skipToken: 'skipfirst100',
$skipToken: 'aaa', };
}; const response2 = createMockARGSubscriptionResponse();
const response2 = createMockARGSubscriptionResponse(); const { resourcePickerData, postResource } = createResourcePickerData([response1, response2]);
postResource = jest.fn();
postResource.mockResolvedValueOnce(response1);
postResource.mockResolvedValueOnce(response2);
resourcePickerData.postResource = postResource;
});
it('should requests additional pages', async () => { await resourcePickerData.getSubscriptions();
await resourcePickerData.getSubscriptions();
expect(postResource).toHaveBeenCalledTimes(2);
});
it('should use the skipToken of the previous page', async () => { expect(postResource).toHaveBeenCalledTimes(2);
await resourcePickerData.getSubscriptions(); const secondCall = postResource.mock.calls[1];
const secondCall = postResource.mock.calls[1]; const [_, postBody] = secondCall;
expect(secondCall[1]).toMatchObject({ options: { $skipToken: 'aaa', resultFormat: 'objectArray' } }); expect(postBody.options.$skipToken).toEqual('skipfirst100');
});
it('returns a concatenates a formatted array of subscriptions when there are multiple pages from arg', async () => {
const response1 = {
...createMockARGSubscriptionResponse(),
$skipToken: 'skipfirst100',
};
const response2 = createMockARGSubscriptionResponse();
const { resourcePickerData } = createResourcePickerData([response1, response2]);
const subscriptions = await resourcePickerData.getSubscriptions();
expect(subscriptions.length).toEqual(12);
expect(subscriptions[0]).toEqual({
id: '1',
name: 'Primary Subscription',
type: 'Subscription',
typeLabel: 'Subscription',
uri: '/subscriptions/1',
children: [],
}); });
}); });
it('throws an error if it does not recieve data from arg', async () => {
const mockResponse = { data: [] };
const { resourcePickerData } = createResourcePickerData([mockResponse]);
try {
await resourcePickerData.getSubscriptions();
throw Error('expected getSubscriptions to fail but it succeeded');
} catch (err) {
expect(err.message).toEqual('unable to fetch subscriptions');
}
});
}); });
describe('getResourcesForResourceGroup', () => { describe('getResourceGroupsBySubscriptionId', () => {
beforeEach(() => { it('makes 1 call to ARG with the correct path and query arguments', async () => {
postResource = jest.fn().mockResolvedValue(createMockARGResourceGroupsResponse()); const mockResponse = createMockARGResourceGroupsResponse();
resourcePickerData.postResource = postResource; const { resourcePickerData, postResource } = createResourcePickerData([mockResponse]);
});
it('calls ARG API', async () => {
await resourcePickerData.getResourceGroupsBySubscriptionId('123'); await resourcePickerData.getResourceGroupsBySubscriptionId('123');
expect(postResource).toHaveBeenCalled(); expect(postResource).toBeCalledTimes(1);
const argQuery = postResource.mock.calls[0][1].query; const firstCall = postResource.mock.calls[0];
const [path, postBody] = firstCall;
expect(path).toEqual('resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01');
expect(postBody.query).toContain("type == 'microsoft.resources/subscriptions/resourcegroups'");
expect(postBody.query).toContain("where subscriptionId == '123'");
});
it('returns formatted resourceGroups', async () => {
const mockResponse = createMockARGResourceGroupsResponse();
const { resourcePickerData } = createResourcePickerData([mockResponse]);
expect(argQuery).toContain(`| where subscriptionId == '123'`); const resourceGroups = await resourcePickerData.getResourceGroupsBySubscriptionId('123');
expect(resourceGroups.length).toEqual(6);
expect(resourceGroups[0]).toEqual({
id: 'prod',
name: 'Production',
type: 'ResourceGroup',
typeLabel: 'Resource Group',
uri: '/subscriptions/abc-123/resourceGroups/prod',
children: [],
});
}); });
describe('when there is more than one page', () => { it('makes multiple requests when it is returned a skip token', async () => {
beforeEach(() => { const response1 = {
const response1 = { ...createMockARGResourceGroupsResponse(),
...createMockARGResourceGroupsResponse(), $skipToken: 'skipfirst100',
$skipToken: 'aaa', };
}; const response2 = createMockARGResourceGroupsResponse();
const response2 = createMockARGResourceGroupsResponse(); const { resourcePickerData, postResource } = createResourcePickerData([response1, response2]);
postResource = jest.fn();
postResource.mockResolvedValueOnce(response1);
postResource.mockResolvedValueOnce(response2);
resourcePickerData.postResource = postResource;
});
it('should requests additional pages', async () => { await resourcePickerData.getResourceGroupsBySubscriptionId('123');
await resourcePickerData.getResourceGroupsBySubscriptionId('123');
expect(postResource).toHaveBeenCalledTimes(2);
});
it('should use the skipToken of the previous page', async () => { expect(postResource).toHaveBeenCalledTimes(2);
await resourcePickerData.getResourceGroupsBySubscriptionId('123'); const secondCall = postResource.mock.calls[1];
const secondCall = postResource.mock.calls[1]; const [_, postBody] = secondCall;
expect(secondCall[1]).toMatchObject({ options: { $skipToken: 'aaa', resultFormat: 'objectArray' } }); expect(postBody.options.$skipToken).toEqual('skipfirst100');
});
it('returns a concatonized and formatted array of resourceGroups when there are multiple pages', async () => {
const response1 = {
...createMockARGResourceGroupsResponse(),
$skipToken: 'skipfirst100',
};
const response2 = createMockARGResourceGroupsResponse();
const { resourcePickerData } = createResourcePickerData([response1, response2]);
const resourceGroups = await resourcePickerData.getResourceGroupsBySubscriptionId('123');
expect(resourceGroups.length).toEqual(12);
expect(resourceGroups[0]).toEqual({
id: 'prod',
name: 'Production',
type: 'ResourceGroup',
typeLabel: 'Resource Group',
uri: '/subscriptions/abc-123/resourceGroups/prod',
children: [],
}); });
}); });
it('throws an error if it does not receive data', async () => {
const mockResponse = { data: [] };
const { resourcePickerData } = createResourcePickerData([mockResponse]);
try {
await resourcePickerData.getResourceGroupsBySubscriptionId('123');
throw Error('expected getSubscriptions to fail but it succeeded');
} catch (err) {
expect(err.message).toEqual('unable to fetch resource groups');
}
});
it('throws an error if it recieves data with a malformed uri', async () => {
const mockResponse = {
data: [
{
resourceGroupURI: '/a-differently-formatted/uri/than/the/type/we/planned/to/parse',
resourceGroupName: 'Production',
},
],
};
const { resourcePickerData } = createResourcePickerData([mockResponse]);
try {
await resourcePickerData.getResourceGroupsBySubscriptionId('123');
throw Error('expected getResourceGroupsBySubscriptionId to fail but it succeeded');
} catch (err) {
expect(err.message).toEqual('unable to fetch resource groups');
}
});
}); });
describe('getResourcesForResourceGroup', () => { describe('getResourcesForResourceGroup', () => {
const resourceRow = { it('makes 1 call to ARG with the correct path and query arguments', async () => {
id: '/subscriptions/def-456/resourceGroups/dev', const mockResponse = createARGResourcesResponse();
name: 'Dev', const { resourcePickerData, postResource } = createResourcePickerData([mockResponse]);
type: ResourceRowType.ResourceGroup, await resourcePickerData.getResourcesForResourceGroup('dev');
typeLabel: 'Resource group',
};
beforeEach(() => { expect(postResource).toBeCalledTimes(1);
postResource = jest.fn().mockResolvedValue(createARGResourcesResponse()); const firstCall = postResource.mock.calls[0];
resourcePickerData.postResource = postResource; const [path, postBody] = firstCall;
expect(path).toEqual('resourcegraph/providers/Microsoft.ResourceGraph/resources?api-version=2021-03-01');
expect(postBody.query).toContain('resources');
expect(postBody.query).toContain('where id hasprefix "dev"');
}); });
it('requests resources for the specified resource row', async () => {
await resourcePickerData.getResourcesForResourceGroup(resourceRow.id);
expect(postResource).toHaveBeenCalled();
const argQuery = postResource.mock.calls[0][1].query;
expect(argQuery).toContain(resourceRow.id);
});
it('returns formatted resources', async () => { it('returns formatted resources', async () => {
const results = await resourcePickerData.getResourcesForResourceGroup(resourceRow.id); const mockResponse = createARGResourcesResponse();
const { resourcePickerData } = createResourcePickerData([mockResponse]);
expect(results.map((v) => v.id)).toEqual([ const resources = await resourcePickerData.getResourcesForResourceGroup('dev');
'/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/web-server',
'/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/disks/web-server_DataDisk',
'/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/db-server',
'/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/disks/db-server_DataDisk',
]);
results.forEach((v) => expect(v.type).toEqual(ResourceRowType.Resource)); expect(resources.length).toEqual(4);
expect(resources[0]).toEqual({
id: 'web-server',
name: 'web-server',
type: 'Resource',
location: 'North Europe',
resourceGroupName: 'dev',
typeLabel: 'Microsoft.Compute/virtualMachines',
uri: '/subscriptions/def-456/resourceGroups/dev/providers/Microsoft.Compute/virtualMachines/web-server',
});
});
it('throws an error if it recieves data with a malformed uri', async () => {
const mockResponse = {
data: [
{
id: '/a-differently-formatted/uri/than/the/type/we/planned/to/parse',
name: 'web-server',
type: 'Microsoft.Compute/virtualMachines',
resourceGroup: 'dev',
subscriptionId: 'def-456',
location: 'northeurope',
},
],
};
const { resourcePickerData } = createResourcePickerData([mockResponse]);
try {
await resourcePickerData.getResourcesForResourceGroup('dev');
throw Error('expected getResourcesForResourceGroup to fail but it succeeded');
} catch (err) {
expect(err.message).toEqual('unable to fetch resource details');
}
}); });
}); });
}); });

View File

@ -7,7 +7,7 @@ import {
logsSupportedResourceTypesKusto, logsSupportedResourceTypesKusto,
resourceTypeDisplayNames, resourceTypeDisplayNames,
} from '../azureMetadata'; } from '../azureMetadata';
import { ResourceRow, ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types'; import { ResourceRowGroup, ResourceRowType } from '../components/ResourcePicker/types';
import { parseResourceURI } from '../components/ResourcePicker/utils'; import { parseResourceURI } from '../components/ResourcePicker/utils';
import { import {
AzureDataSourceJsonData, AzureDataSourceJsonData,
@ -31,8 +31,6 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
this.resourcePath = `${routeNames.resourceGraph}`; this.resourcePath = `${routeNames.resourceGraph}`;
} }
static readonly templateVariableGroupID = '$$grafana-templateVariables$$';
async getSubscriptions(): Promise<ResourceRowGroup> { async getSubscriptions(): Promise<ResourceRowGroup> {
const query = ` const query = `
resources resources
@ -59,7 +57,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
} }
const resourceResponse = await this.makeResourceGraphRequest<RawAzureSubscriptionItem[]>(query, 1, options); const resourceResponse = await this.makeResourceGraphRequest<RawAzureSubscriptionItem[]>(query, 1, options);
if (!resourceResponse.data.length) { if (!resourceResponse.data.length) {
throw new Error('unable to fetch resource details'); throw new Error('unable to fetch subscriptions');
} }
resources = resources.concat(resourceResponse.data); resources = resources.concat(resourceResponse.data);
$skipToken = resourceResponse.$skipToken; $skipToken = resourceResponse.$skipToken;
@ -69,13 +67,14 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
return resources.map((subscription) => ({ return resources.map((subscription) => ({
name: subscription.subscriptionName, name: subscription.subscriptionName,
id: subscription.subscriptionId, id: subscription.subscriptionId,
uri: `/subscriptions/${subscription.subscriptionId}`,
typeLabel: 'Subscription', typeLabel: 'Subscription',
type: ResourceRowType.Subscription, type: ResourceRowType.Subscription,
children: [], children: [],
})); }));
} }
async getResourceGroupsBySubscriptionId(subscriptionId: string) { async getResourceGroupsBySubscriptionId(subscriptionId: string): Promise<ResourceRowGroup> {
const query = ` const query = `
resources resources
| join kind=inner ( | join kind=inner (
@ -89,7 +88,7 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
| summarize count() by resourceGroupName, resourceGroupURI | summarize count() by resourceGroupName, resourceGroupURI
| order by resourceGroupURI asc`; | order by resourceGroupURI asc`;
let resources: RawAzureResourceGroupItem[] = []; let resourceGroups: RawAzureResourceGroupItem[] = [];
let allFetched = false; let allFetched = false;
let $skipToken = undefined; let $skipToken = undefined;
while (!allFetched) { while (!allFetched) {
@ -102,32 +101,54 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
} }
const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query, 1, options); const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query, 1, options);
if (!resourceResponse.data.length) { if (!resourceResponse.data.length) {
throw new Error('unable to fetch resource details'); throw new Error('unable to fetch resource groups');
} }
resources = resources.concat(resourceResponse.data); resourceGroups = resourceGroups.concat(resourceResponse.data);
$skipToken = resourceResponse.$skipToken; $skipToken = resourceResponse.$skipToken;
allFetched = !$skipToken; allFetched = !$skipToken;
} }
return resources.map((r) => ({ return resourceGroups.map((r) => {
name: r.resourceGroupName, const parsedUri = parseResourceURI(r.resourceGroupURI);
id: r.resourceGroupURI, if (!parsedUri || !parsedUri.resourceGroup) {
type: ResourceRowType.ResourceGroup, throw new Error('unable to fetch resource groups');
typeLabel: 'Resource Group', }
children: [], return {
})); name: r.resourceGroupName,
uri: r.resourceGroupURI,
id: parsedUri.resourceGroup,
type: ResourceRowType.ResourceGroup,
typeLabel: 'Resource Group',
children: [],
};
});
} }
async getResourcesForResourceGroup(resourceGroupId: string) { async getResourcesForResourceGroup(resourceGroupId: string): Promise<ResourceRowGroup> {
const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(` const { data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
resources resources
| where id hasprefix "${resourceGroupId}" | where id hasprefix "${resourceGroupId}"
| where type in (${logsSupportedResourceTypesKusto}) and location in (${logsSupportedLocationsKusto}) | where type in (${logsSupportedResourceTypesKusto}) and location in (${logsSupportedLocationsKusto})
`); `);
return formatResourceGroupChildren(response); return response.map((item) => {
const parsedUri = parseResourceURI(item.id);
if (!parsedUri || !parsedUri.resource) {
throw new Error('unable to fetch resource details');
}
return {
name: item.name,
id: parsedUri.resource,
uri: item.id,
resourceGroupName: item.resourceGroup,
type: ResourceRowType.Resource,
typeLabel: resourceTypeDisplayNames[item.type] || item.type,
location: locationDisplayNames[item.location] || item.location,
};
});
} }
// used to make the select resource button that launches the resource picker show a nicer file path to users
async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> { async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
const { subscriptionID, resourceGroup } = parseResourceURI(resourceURI) ?? {}; const { subscriptionID, resourceGroup } = parseResourceURI(resourceURI) ?? {};
@ -206,30 +227,4 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
throw error; throw error;
} }
} }
transformVariablesToRow(templateVariables: string[]): ResourceRow {
return {
id: ResourcePickerData.templateVariableGroupID,
name: 'Template variables',
type: ResourceRowType.VariableGroup,
typeLabel: 'Variables',
children: templateVariables.map((v) => ({
id: v,
name: v,
type: ResourceRowType.Variable,
typeLabel: 'Variable',
})),
};
}
}
function formatResourceGroupChildren(rawData: RawAzureResourceItem[]): ResourceRowGroup {
return rawData.map((item) => ({
name: item.name,
id: item.id,
resourceGroupName: item.resourceGroup,
type: ResourceRowType.Resource,
typeLabel: resourceTypeDisplayNames[item.type] || item.type,
location: locationDisplayNames[item.location] || item.location,
}));
} }