diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx index 9c368f9cb1f..3d57a17ccf0 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx @@ -3,7 +3,7 @@ import NestedResourceTable from './NestedResourceTable'; import { ResourceRow, ResourceRowGroup } from './types'; import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -import { Button, useStyles2 } from '@grafana/ui'; +import { Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import ResourcePickerData from '../../resourcePicker/resourcePickerData'; import { Space } from '../Space'; import { addResources, findRow, parseResourceURI } from './utils'; @@ -28,6 +28,7 @@ const ResourcePicker = ({ const [azureRows, setAzureRows] = useState([]); const [internalSelected, setInternalSelected] = useState(resourceURI); + const [isLoading, setIsLoading] = useState(false); // Sync the resourceURI prop to internal state useEffect(() => { @@ -76,7 +77,9 @@ const ResourcePicker = ({ // Request initial data on first mount useEffect(() => { + setIsLoading(true); resourcePickerData.getResourcePickerData().then((initalRows) => { + setIsLoading(false); setAzureRows(initalRows); }); }, [resourcePickerData]); @@ -111,36 +114,44 @@ const ResourcePicker = ({ return (
- + {isLoading ? ( +
+ +
+ ) : ( + <> + + +
+ {selectedResourceRows.length > 0 && ( + <> + +
Selection
+ + + )} -
- {selectedResourceRows.length > 0 && ( - <> -
Selection
- - - )} - - - - - -
+ + + +
+ + )}
); }; @@ -154,4 +165,10 @@ const getStyles = (theme: GrafanaTheme2) => ({ background: theme.colors.background.primary, paddingTop: theme.spacing(2), }), + loadingWrapper: css({ + textAlign: 'center', + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), + color: theme.colors.text.secondary, + }), }); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts index 7403a1068e1..216a9d7b99b 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts @@ -47,6 +47,52 @@ describe('AzureMonitor resourcePickerData', () => { '/subscription/def-456/resourceGroups/qa', ]); }); + + describe('when there is more than one page', () => { + beforeEach(() => { + const response1 = { + ...createMockARGResourceContainersResponse(), + $skipToken: 'aaa', + }; + const response2 = createMockARGResourceContainersResponse(); + postResource = jest.fn(); + postResource.mockResolvedValueOnce(response1); + postResource.mockResolvedValueOnce(response2); + resourcePickerData.postResource = postResource; + }); + + it('should requests additional pages', async () => { + await resourcePickerData.getResourcePickerData(); + expect(postResource).toHaveBeenCalledTimes(2); + }); + + it('should use the skipToken of the previous page', async () => { + await resourcePickerData.getResourcePickerData(); + const secondCall = postResource.mock.calls[1]; + expect(secondCall[1]).toMatchObject({ options: { $skipToken: 'aaa', resultFormat: 'objectArray' } }); + }); + + it('should combine responses', async () => { + const results = await resourcePickerData.getResourcePickerData(); + expect(results[0].children?.map((v) => v.id)).toEqual([ + '/subscriptions/abc-123/resourceGroups/prod', + '/subscriptions/abc-123/resourceGroups/pre-prod', + // second page + '/subscriptions/abc-123/resourceGroups/prod', + '/subscriptions/abc-123/resourceGroups/pre-prod', + ]); + + expect(results[1].children?.map((v) => v.id)).toEqual([ + '/subscription/def-456/resourceGroups/dev', + '/subscription/def-456/resourceGroups/test', + '/subscription/def-456/resourceGroups/qa', + // second page + '/subscription/def-456/resourceGroups/dev', + '/subscription/def-456/resourceGroups/test', + '/subscription/def-456/resourceGroups/qa', + ]); + }); + }); }); describe('getResourcesForResourceGroup', () => { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts index a535cf0cc94..55f6047ea56 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts @@ -12,6 +12,7 @@ import { AzureDataSourceJsonData, AzureGraphResponse, AzureMonitorQuery, + AzureResourceGraphOptions, AzureResourceSummaryItem, RawAzureResourceGroupItem, RawAzureResourceItem, @@ -54,9 +55,27 @@ export default class ResourcePickerData extends DataSourceWithBackend(query); + let resources: RawAzureResourceGroupItem[] = []; + let allFetched = false; + let $skipToken = undefined; + while (!allFetched) { + // The response may include several pages + let options: Partial = {}; + if ($skipToken) { + options = { + $skipToken, + }; + } + const resourceResponse = await this.makeResourceGraphRequest(query, 1, options); + if (!resourceResponse.data.length) { + throw new Error('unable to fetch resource details'); + } + resources = resources.concat(resourceResponse.data); + $skipToken = resourceResponse.$skipToken; + allFetched = !$skipToken; + } - return formatResourceGroupData(response.data); + return formatResourceGroupData(resources); } async getResourcesForResourceGroup(resourceGroup: ResourceRow) { @@ -126,12 +145,17 @@ export default class ResourcePickerData extends DataSourceWithBackend(query: string, maxRetries = 1): Promise> { + async makeResourceGraphRequest( + query: string, + maxRetries = 1, + reqOptions?: Partial + ): Promise> { try { return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, { query: query, options: { resultFormat: 'objectArray', + ...reqOptions, }, }); } catch (error) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts index 719b6e92648..fb226f20e75 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts @@ -177,4 +177,15 @@ export interface RawAzureResourceItem { export interface AzureGraphResponse { data: T; + // skipToken is used for pagination, to get the next page + $skipToken?: string; +} + +// https://docs.microsoft.com/en-us/rest/api/azureresourcegraph/resourcegraph(2021-03-01)/resources/resources#queryrequestoptions +export interface AzureResourceGraphOptions { + $skip: number; + $skipToken: string; + $top: number; + allowPartialScopes: boolean; + resultFormat: 'objectArray' | 'table'; }