Skip to content

Commit

Permalink
AzureMonitor: Support querying Subscriptions and Resource Groups in L…
Browse files Browse the repository at this point in the history
…ogs (#34766)

* AzureMonitor: Support querying Subscriptions and Resource Groups in Logs

* cleanup

(cherry picked from commit 888cddb)
  • Loading branch information
joshhunt authored and grafanabot committed May 28, 2021
1 parent 638735a commit 45ed259
Show file tree
Hide file tree
Showing 8 changed files with 116 additions and 35 deletions.
Expand Up @@ -17,10 +17,9 @@ function parseResourceDetails(resourceURI: string) {
}

return {
id: resourceURI,
subscriptionName: parsed.subscriptionID,
resourceGroupName: parsed.resourceGroup,
name: parsed.resource,
resourceName: parsed.resource,
};
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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>
);
};
Expand Down
@@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -185,20 +188,24 @@ 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'}
aria-label={isOpen ? 'Collapse' : 'Expand'}
onClick={handleToggleCollapse}
id={entry.id}
/>
) : (
<Space layout="inline" h={2} />
)}
</span>

{isSelectable && (
{isSelectable && (
<span className={styles.entryContentItem}>
<Checkbox id={checkboxId} onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} />
)}
</span>
</span>
)}

<span className={styles.entryContentItem}>
<EntryIcon entry={entry} isOpen={isOpen} />
Expand Down
Expand Up @@ -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
Expand Down
Expand Up @@ -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),
},
}),

Expand Down
@@ -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();
});
});
});
@@ -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 };
}

Expand Down
@@ -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,
Expand Down Expand Up @@ -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);
Expand Down
Expand Up @@ -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 {
Expand Down

0 comments on commit 45ed259

Please sign in to comment.