mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 18:01:40 -06:00
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:
parent
ee73108e52
commit
888cddb834
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
},
|
||||
}),
|
||||
|
||||
|
@ -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 };
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user