AzureMonitor: Request and concat subsequent resource pages (#36958)

pull/36634/head^2
Andres Martinez Gotor 4 years ago committed by GitHub
parent 1a52dd57cf
commit 5a0221a8c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 77
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx
  2. 46
      public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.test.ts
  3. 30
      public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts
  4. 11
      public/app/plugins/datasource/grafana-azure-monitor-datasource/types/types.ts

@ -3,7 +3,7 @@ import NestedResourceTable from './NestedResourceTable';
import { ResourceRow, ResourceRowGroup } from './types'; import { ResourceRow, ResourceRowGroup } from './types';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { Button, useStyles2 } from '@grafana/ui'; import { Button, LoadingPlaceholder, useStyles2 } from '@grafana/ui';
import ResourcePickerData from '../../resourcePicker/resourcePickerData'; import ResourcePickerData from '../../resourcePicker/resourcePickerData';
import { Space } from '../Space'; import { Space } from '../Space';
import { addResources, findRow, parseResourceURI } from './utils'; import { addResources, findRow, parseResourceURI } from './utils';
@ -28,6 +28,7 @@ const ResourcePicker = ({
const [azureRows, setAzureRows] = useState<ResourceRowGroup>([]); const [azureRows, setAzureRows] = useState<ResourceRowGroup>([]);
const [internalSelected, setInternalSelected] = useState<string | undefined>(resourceURI); const [internalSelected, setInternalSelected] = useState<string | undefined>(resourceURI);
const [isLoading, setIsLoading] = useState(false);
// Sync the resourceURI prop to internal state // Sync the resourceURI prop to internal state
useEffect(() => { useEffect(() => {
@ -76,7 +77,9 @@ const ResourcePicker = ({
// Request initial data on first mount // Request initial data on first mount
useEffect(() => { useEffect(() => {
setIsLoading(true);
resourcePickerData.getResourcePickerData().then((initalRows) => { resourcePickerData.getResourcePickerData().then((initalRows) => {
setIsLoading(false);
setAzureRows(initalRows); setAzureRows(initalRows);
}); });
}, [resourcePickerData]); }, [resourcePickerData]);
@ -111,36 +114,44 @@ const ResourcePicker = ({
return ( return (
<div> <div>
<NestedResourceTable {isLoading ? (
rows={rows} <div className={styles.loadingWrapper}>
requestNestedRows={requestNestedRows} <LoadingPlaceholder text={'Loading resources...'} />
onRowSelectedChange={handleSelectionChanged} </div>
selectedRows={selectedResourceRows} ) : (
/> <>
<NestedResourceTable
<div className={styles.selectionFooter}> rows={rows}
{selectedResourceRows.length > 0 && ( requestNestedRows={requestNestedRows}
<> onRowSelectedChange={handleSelectionChanged}
selectedRows={selectedResourceRows}
/>
<div className={styles.selectionFooter}>
{selectedResourceRows.length > 0 && (
<>
<Space v={2} />
<h5>Selection</h5>
<NestedResourceTable
rows={selectedResourceRows}
requestNestedRows={requestNestedRows}
onRowSelectedChange={handleSelectionChanged}
selectedRows={selectedResourceRows}
noHeader={true}
/>
</>
)}
<Space v={2} /> <Space v={2} />
<h5>Selection</h5>
<NestedResourceTable <Button onClick={handleApply}>Apply</Button>
rows={selectedResourceRows} <Space layout="inline" h={1} />
requestNestedRows={requestNestedRows} <Button onClick={onCancel} variant="secondary">
onRowSelectedChange={handleSelectionChanged} Cancel
selectedRows={selectedResourceRows} </Button>
noHeader={true} </div>
/> </>
</> )}
)}
<Space v={2} />
<Button onClick={handleApply}>Apply</Button>
<Space layout="inline" h={1} />
<Button onClick={onCancel} variant="secondary">
Cancel
</Button>
</div>
</div> </div>
); );
}; };
@ -154,4 +165,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
background: theme.colors.background.primary, background: theme.colors.background.primary,
paddingTop: theme.spacing(2), paddingTop: theme.spacing(2),
}), }),
loadingWrapper: css({
textAlign: 'center',
paddingTop: theme.spacing(2),
paddingBottom: theme.spacing(2),
color: theme.colors.text.secondary,
}),
}); });

@ -47,6 +47,52 @@ describe('AzureMonitor resourcePickerData', () => {
'/subscription/def-456/resourceGroups/qa', '/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', () => { describe('getResourcesForResourceGroup', () => {

@ -12,6 +12,7 @@ import {
AzureDataSourceJsonData, AzureDataSourceJsonData,
AzureGraphResponse, AzureGraphResponse,
AzureMonitorQuery, AzureMonitorQuery,
AzureResourceGraphOptions,
AzureResourceSummaryItem, AzureResourceSummaryItem,
RawAzureResourceGroupItem, RawAzureResourceGroupItem,
RawAzureResourceItem, RawAzureResourceItem,
@ -54,9 +55,27 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
| order by subscriptionURI asc | order by subscriptionURI asc
`; `;
const response = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(query); let resources: RawAzureResourceGroupItem[] = [];
let allFetched = false;
let $skipToken = undefined;
while (!allFetched) {
// The response may include several pages
let options: Partial<AzureResourceGraphOptions> = {};
if ($skipToken) {
options = {
$skipToken,
};
}
const resourceResponse = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(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) { async getResourcesForResourceGroup(resourceGroup: ResourceRow) {
@ -126,12 +145,17 @@ export default class ResourcePickerData extends DataSourceWithBackend<AzureMonit
return response[0].id; return response[0].id;
} }
async makeResourceGraphRequest<T = unknown>(query: string, maxRetries = 1): Promise<AzureGraphResponse<T>> { async makeResourceGraphRequest<T = unknown>(
query: string,
maxRetries = 1,
reqOptions?: Partial<AzureResourceGraphOptions>
): Promise<AzureGraphResponse<T>> {
try { try {
return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, { return await this.postResource(this.resourcePath + RESOURCE_GRAPH_URL, {
query: query, query: query,
options: { options: {
resultFormat: 'objectArray', resultFormat: 'objectArray',
...reqOptions,
}, },
}); });
} catch (error) { } catch (error) {

@ -177,4 +177,15 @@ export interface RawAzureResourceItem {
export interface AzureGraphResponse<T = unknown> { export interface AzureGraphResponse<T = unknown> {
data: T; 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';
} }

Loading…
Cancel
Save