diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx index 2a2254319085..7d69c8ab17bd 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/LogsQueryEditor/ResourceField.tsx @@ -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 ( {resource.subscriptionName} - - {resource.resourceGroupName} - - {resource.name} + {resource.resourceGroupName && ( + <> + + {resource.resourceGroupName} + + )} + {resource.resourceName && ( + <> + + {resource.resourceName} + + )} ); }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx index a238b9e5e9b5..78407c3ba5b3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/NestedRows.tsx @@ -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 = ({ 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 = ({ of the collapse button for leaf rows that have no children to get them to align */} - {hasChildren && ( + {hasChildren ? ( = ({ onClick={handleToggleCollapse} id={entry.id} /> + ) : ( + )} + - {isSelectable && ( + {isSelectable && ( + - )} - + + )} 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 c4f0d88e68c7..9c368f9cb1fd 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 @@ -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 diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts index 95f95ee70252..dc039a859c87 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/styles.ts @@ -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), }, }), diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts new file mode 100644 index 000000000000..58dd86f5be82 --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.test.ts @@ -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(); + }); + }); +}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts index ab9f0af4b24b..5d6d8fdd5984 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/components/ResourcePicker/utils.ts @@ -1,16 +1,23 @@ import produce from 'immer'; import { ResourceRow, ResourceRowGroup } from './types'; -const RESOURCE_URI_REGEX = /\/subscriptions\/(?.+)\/resourceGroups\/(?.+)\/providers.+\/(?[\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\/(?[^/]+)(?:\/resourceGroups\/(?[^/]+)(?:\/providers.+\/(?[^/]+))?)?/; + +type RegexGroups = Record; 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 }; } 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 87af244c534c..4ee61891a775 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 @@ -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 { + 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(query); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts index 22b6c64dc7d6..cb696c186dad 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/types/index.ts @@ -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 {