AzureMonitor: Support querying Subscriptions and Resource Groups in Logs (#34766)

* AzureMonitor: Support querying Subscriptions and Resource Groups in Logs

* cleanup
This commit is contained in:
Josh Hunt 2021-05-28 08:58:46 +01:00 committed by GitHub
parent ee73108e52
commit 888cddb834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 117 additions and 36 deletions

View File

@ -17,10 +17,9 @@ function parseResourceDetails(resourceURI: string) {
}
return {
id: resourceURI,
subscriptionName: parsed.subscriptionID,
resourceGroupName: parsed.resourceGroup,
name: parsed.resource,
resourceName: parsed.resource,
};
}
@ -85,7 +84,7 @@ const ResourceLabel = ({ resource, datasource }: ResourceLabelProps) => {
useEffect(() => {
if (resource && parseResourceDetails(resource)) {
datasource.resourcePickerData.getResource(resource).then(setResourceComponents);
datasource.resourcePickerData.getResourceURIDisplayProperties(resource).then(setResourceComponents);
} else {
setResourceComponents(undefined);
}
@ -118,10 +117,18 @@ const FormattedResource = ({ resource }: FormattedResourceProps) => {
return (
<span>
<Icon name="layer-group" /> {resource.subscriptionName}
<Separator />
<Icon name="folder" /> {resource.resourceGroupName}
<Separator />
<Icon name="cube" /> {resource.name}
{resource.resourceGroupName && (
<>
<Separator />
<Icon name="folder" /> {resource.resourceGroupName}
</>
)}
{resource.resourceName && (
<>
<Separator />
<Icon name="cube" /> {resource.resourceName}
</>
)}
</span>
);
};

View File

@ -1,6 +1,7 @@
import { cx } from '@emotion/css';
import { Checkbox, Icon, IconButton, LoadingPlaceholder, useStyles2, useTheme2, FadeTransition } from '@grafana/ui';
import React, { useCallback, useEffect, useState } from 'react';
import { Space } from '../Space';
import getStyles from './styles';
import { ResourceRowType, ResourceRow, ResourceRowGroup } from './types';
import { findRow } from './utils';
@ -163,7 +164,9 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
const theme = useTheme2();
const styles = useStyles2(getStyles);
const hasChildren = !!entry.children;
const isSelectable = entry.type === ResourceRowType.Resource || entry.type === ResourceRowType.Variable;
// Subscriptions, resource groups, resources, and variables are all selectable, so
// the top-level variable group is the only thing that cannot be selected.
const isSelectable = entry.type !== ResourceRowType.VariableGroup;
const handleToggleCollapse = useCallback(() => {
onToggleCollapse(entry);
@ -185,7 +188,7 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
of the collapse button for leaf rows that have no children to get them to align */}
<span className={styles.entryContentItem}>
{hasChildren && (
{hasChildren ? (
<IconButton
className={styles.collapseButton}
name={isOpen ? 'angle-down' : 'angle-right'}
@ -193,13 +196,17 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
onClick={handleToggleCollapse}
id={entry.id}
/>
)}
{isSelectable && (
<Checkbox id={checkboxId} onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} />
) : (
<Space layout="inline" h={2} />
)}
</span>
{isSelectable && (
<span className={styles.entryContentItem}>
<Checkbox id={checkboxId} onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} />
</span>
)}
<span className={styles.entryContentItem}>
<EntryIcon entry={entry} isOpen={isOpen} />
</span>

View File

@ -42,7 +42,14 @@ const ResourcePicker = ({
// Map the selected item into an array of rows
const selectedResourceRows = useMemo(() => {
const found = internalSelected && findRow(rows, internalSelected);
return found ? [found] : [];
return found
? [
{
...found,
children: undefined,
},
]
: [];
}, [internalSelected, rows]);
// Request resources for a expanded resource group

View File

@ -29,13 +29,13 @@ const getStyles = (theme: GrafanaTheme2) => ({
}),
cell: css({
padding: theme.spacing(1, 0),
padding: theme.spacing(1, 1, 1, 0),
width: '25%',
overflow: 'hidden',
textOverflow: 'ellipsis',
'&:first-of-type': {
width: '50%',
padding: theme.spacing(1, 0, 1, 2),
padding: theme.spacing(1, 1, 1, 2),
},
}),

View File

@ -0,0 +1,36 @@
import { parseResourceURI } from './utils';
describe('AzureMonitor ResourcePicker utils', () => {
describe('parseResourceURI', () => {
it('should parse subscription URIs', () => {
expect(parseResourceURI('/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572')).toEqual({
subscriptionID: '44693801-6ee6-49de-9b2d-9106972f9572',
});
});
it('should parse resource group URIs', () => {
expect(
parseResourceURI('/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources')
).toEqual({
subscriptionID: '44693801-6ee6-49de-9b2d-9106972f9572',
resourceGroup: 'cloud-datasources',
});
});
it('should parse resource URIs', () => {
expect(
parseResourceURI(
'/subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources/providers/Microsoft.Compute/virtualMachines/GithubTestDataVM'
)
).toEqual({
subscriptionID: '44693801-6ee6-49de-9b2d-9106972f9572',
resourceGroup: 'cloud-datasources',
resource: 'GithubTestDataVM',
});
});
it('returns undefined for invalid input', () => {
expect(parseResourceURI('44693801-6ee6-49de-9b2d-9106972f9572')).toBeUndefined();
});
});
});

View File

@ -1,16 +1,23 @@
import produce from 'immer';
import { ResourceRow, ResourceRowGroup } from './types';
const RESOURCE_URI_REGEX = /\/subscriptions\/(?<subscriptionID>.+)\/resourceGroups\/(?<resourceGroup>.+)\/providers.+\/(?<resource>[\w-_]+)/;
// This regex matches URIs representing:
// - subscriptions: /subscriptions/44693801-6ee6-49de-9b2d-9106972f9572
// - resource groups: /subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources
// - resources: /subscriptions/44693801-6ee6-49de-9b2d-9106972f9572/resourceGroups/cloud-datasources/providers/Microsoft.Compute/virtualMachines/GithubTestDataVM
const RESOURCE_URI_REGEX = /\/subscriptions\/(?<subscriptionID>[^/]+)(?:\/resourceGroups\/(?<resourceGroup>[^/]+)(?:\/providers.+\/(?<resource>[^/]+))?)?/;
type RegexGroups = Record<string, string | undefined>;
export function parseResourceURI(resourceURI: string) {
const matches = RESOURCE_URI_REGEX.exec(resourceURI);
const groups: RegexGroups = matches?.groups ?? {};
const { subscriptionID, resourceGroup, resource } = groups;
if (!matches?.groups?.subscriptionID || !matches?.groups?.resourceGroup) {
if (!subscriptionID) {
return undefined;
}
const { subscriptionID, resourceGroup, resource } = matches.groups;
return { subscriptionID, resourceGroup, resource };
}

View File

@ -1,6 +1,7 @@
import { FetchResponse, getBackendSrv } from '@grafana/runtime';
import { getLogAnalyticsResourcePickerApiRoute } from '../api/routes';
import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types';
import { parseResourceURI } from '../components/ResourcePicker/utils';
import { getAzureCloud } from '../credentials';
import {
AzureDataSourceInstanceSettings,
@ -73,21 +74,38 @@ export default class ResourcePickerData {
return formatResourceGroupChildren(response.data);
}
async getResource(resourceURI: string) {
async getResourceURIDisplayProperties(resourceURI: string): Promise<AzureResourceSummaryItem> {
const { subscriptionID, resourceGroup } = parseResourceURI(resourceURI) ?? {};
if (!subscriptionID) {
throw new Error('Invalid resource URI passed');
}
// resourceGroupURI and resourceURI could be invalid values, but that's okay because the join
// will just silently fail as expected
const subscriptionURI = `/subscriptions/${subscriptionID}`;
const resourceGroupURI = `${subscriptionURI}/resourceGroups/${resourceGroup}`;
const query = `
resources
| join (
resourcecontainers
| where type == "microsoft.resources/subscriptions"
| project subscriptionName=name, subscriptionId
) on subscriptionId
| join (
resourcecontainers
| where type == "microsoft.resources/subscriptions/resourcegroups"
| project resourceGroupName=name, resourceGroup
) on resourceGroup
| where id == "${resourceURI}"
| project id, name, subscriptionName, resourceGroupName
resourcecontainers
| where type == "microsoft.resources/subscriptions"
| where id == "${subscriptionURI}"
| project subscriptionName=name, subscriptionId
| join kind=leftouter (
resourcecontainers
| where type == "microsoft.resources/subscriptions/resourcegroups"
| where id == "${resourceGroupURI}"
| project resourceGroupName=name, resourceGroup, subscriptionId
) on subscriptionId
| join kind=leftouter (
resources
| where id == "${resourceURI}"
| project resourceName=name, subscriptionId
) on subscriptionId
| project subscriptionName, resourceGroupName, resourceName
`;
const { ok, data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);

View File

@ -230,10 +230,9 @@ export interface AzureQueryEditorFieldProps {
}
export interface AzureResourceSummaryItem {
id: string;
name: string;
subscriptionName: string;
resourceGroupName: string;
resourceGroupName: string | undefined;
resourceName: string | undefined;
}
export interface RawAzureResourceGroupItem {