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

* AzureMonitor: Support querying Subscriptions and Resource Groups in Logs

* cleanup
pull/34886/head
Josh Hunt 4 years ago committed by GitHub
parent ee73108e52
commit 888cddb834
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx
  2. 17
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx
  3. 9
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/index.tsx
  4. 4
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts
  5. 36
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts
  6. 13
      public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts
  7. 46
      public/app/plugins/datasource/grafana-azure-monitor-datasource/resourcePicker/resourcePickerData.ts
  8. 5
      public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts

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

@ -1,6 +1,7 @@
import { cx } from '@emotion/css'; import { cx } from '@emotion/css';
import { Checkbox, Icon, IconButton, LoadingPlaceholder, useStyles2, useTheme2, FadeTransition } from '@grafana/ui'; import { Checkbox, Icon, IconButton, LoadingPlaceholder, useStyles2, useTheme2, FadeTransition } from '@grafana/ui';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { Space } from '../Space';
import getStyles from './styles'; import getStyles from './styles';
import { ResourceRowType, ResourceRow, ResourceRowGroup } from './types'; import { ResourceRowType, ResourceRow, ResourceRowGroup } from './types';
import { findRow } from './utils'; import { findRow } from './utils';
@ -163,7 +164,9 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
const theme = useTheme2(); const theme = useTheme2();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const hasChildren = !!entry.children; 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(() => { const handleToggleCollapse = useCallback(() => {
onToggleCollapse(entry); 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 */} of the collapse button for leaf rows that have no children to get them to align */}
<span className={styles.entryContentItem}> <span className={styles.entryContentItem}>
{hasChildren && ( {hasChildren ? (
<IconButton <IconButton
className={styles.collapseButton} className={styles.collapseButton}
name={isOpen ? 'angle-down' : 'angle-right'} name={isOpen ? 'angle-down' : 'angle-right'}
@ -193,12 +196,16 @@ const NestedEntry: React.FC<NestedEntryProps> = ({
onClick={handleToggleCollapse} onClick={handleToggleCollapse}
id={entry.id} id={entry.id}
/> />
) : (
<Space layout="inline" h={2} />
)} )}
</span>
{isSelectable && ( {isSelectable && (
<span className={styles.entryContentItem}>
<Checkbox id={checkboxId} onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} /> <Checkbox id={checkboxId} onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} />
)} </span>
</span> )}
<span className={styles.entryContentItem}> <span className={styles.entryContentItem}>
<EntryIcon entry={entry} isOpen={isOpen} /> <EntryIcon entry={entry} isOpen={isOpen} />

@ -42,7 +42,14 @@ const ResourcePicker = ({
// 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 = internalSelected && findRow(rows, internalSelected);
return found ? [found] : []; return found
? [
{
...found,
children: undefined,
},
]
: [];
}, [internalSelected, rows]); }, [internalSelected, rows]);
// Request resources for a expanded resource group // Request resources for a expanded resource group

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

@ -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 produce from 'immer';
import { ResourceRow, ResourceRowGroup } from './types'; 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) { export function parseResourceURI(resourceURI: string) {
const matches = RESOURCE_URI_REGEX.exec(resourceURI); 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; return undefined;
} }
const { subscriptionID, resourceGroup, resource } = matches.groups;
return { subscriptionID, resourceGroup, resource }; return { subscriptionID, resourceGroup, resource };
} }

@ -1,6 +1,7 @@
import { FetchResponse, getBackendSrv } from '@grafana/runtime'; import { FetchResponse, getBackendSrv } from '@grafana/runtime';
import { getLogAnalyticsResourcePickerApiRoute } from '../api/routes'; import { getLogAnalyticsResourcePickerApiRoute } from '../api/routes';
import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types'; import { ResourceRowType, ResourceRow, ResourceRowGroup } from '../components/ResourcePicker/types';
import { parseResourceURI } from '../components/ResourcePicker/utils';
import { getAzureCloud } from '../credentials'; import { getAzureCloud } from '../credentials';
import { import {
AzureDataSourceInstanceSettings, AzureDataSourceInstanceSettings,
@ -73,21 +74,38 @@ export default class ResourcePickerData {
return formatResourceGroupChildren(response.data); 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 = ` const query = `
resources resourcecontainers
| join ( | where type == "microsoft.resources/subscriptions"
resourcecontainers | where id == "${subscriptionURI}"
| where type == "microsoft.resources/subscriptions" | project subscriptionName=name, subscriptionId
| project subscriptionName=name, subscriptionId
) on subscriptionId | join kind=leftouter (
| join ( resourcecontainers
resourcecontainers | where type == "microsoft.resources/subscriptions/resourcegroups"
| where type == "microsoft.resources/subscriptions/resourcegroups" | where id == "${resourceGroupURI}"
| project resourceGroupName=name, resourceGroup | project resourceGroupName=name, resourceGroup, subscriptionId
) on resourceGroup ) on subscriptionId
| where id == "${resourceURI}"
| project id, name, subscriptionName, resourceGroupName | 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); const { ok, data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);

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

Loading…
Cancel
Save