mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AzureMonitor: Support querying any Resource for Logs queries (#33879)
* wip * wip: * ui work for resource picker * disable rows when others are selected * resource picker open button * Azure Monitor: Connect to backend with real data (#34024) * Connect to backend with real data * Fixes from code review. * WIP:Begin thinking about how to make queries with scope determined by the resource picker * More fixes post code review. * Fixes after rebasing * Remove outdated todo * AzureMonitor: Support any resource for Logs queries (#33762) * Apply button for resource picker * scroll table body * secondary cancel button * loading state for nested rows * Display resource components in picker button * fix tests * fix icons * move route function * Migrate from workspace to resource uri for log analytics (#34337) * reword backwards compat comment * remove base url suffix * fix lint error * move migrations to seperate file * cleanup * update regex * cleanup * update plugin routes to use new azure auth type * use AzureDataSourceInstanceSettings alias Co-authored-by: Sarah Zinger <sarahzinger@users.noreply.github.com>
This commit is contained in:
@@ -34,6 +34,11 @@ export default function createMockDatasource() {
|
||||
azureLogAnalyticsDatasource: {
|
||||
getKustoSchema: () => Promise.resolve(),
|
||||
},
|
||||
resourcePickerData: {
|
||||
getResourcePickerData: () => ({}),
|
||||
getResourcesForResourceGroup: () => ({}),
|
||||
getResourceURIFromWorkspace: () => '',
|
||||
},
|
||||
};
|
||||
|
||||
const mockDatasource = _mockDatasource as Datasource;
|
||||
|
||||
@@ -49,3 +49,19 @@ export function getAppInsightsApiRoute(azureCloud: string): string {
|
||||
throw new Error('The cloud not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
export function getLogAnalyticsResourcePickerApiRoute(azureCloud: string) {
|
||||
switch (azureCloud) {
|
||||
case 'azuremonitor':
|
||||
return 'loganalytics-resourcepickerdata';
|
||||
|
||||
case 'govazuremonitor': // Azure US Government
|
||||
return 'loganalytics-resourcepickerdata-gov';
|
||||
|
||||
case 'chinaazuremonitor': // Azure China
|
||||
return 'loganalytics-resourcepickerdata-china';
|
||||
|
||||
default:
|
||||
throw new Error('The cloud not supported.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,10 +69,10 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
return this.doRequest(workspaceListUrl, true);
|
||||
}
|
||||
|
||||
async getMetadata(workspace: string) {
|
||||
const url = `${this.baseUrl}/${getTemplateSrv().replace(workspace, {})}/metadata`;
|
||||
const resp = await this.doRequest<AzureLogAnalyticsMetadata>(url);
|
||||
async getMetadata(resourceUri: string) {
|
||||
const url = `${this.baseUrl}/v1${resourceUri}/metadata`;
|
||||
|
||||
const resp = await this.doRequest<AzureLogAnalyticsMetadata>(url);
|
||||
if (!resp.ok) {
|
||||
throw new Error('Unable to get metadata for workspace');
|
||||
}
|
||||
@@ -80,9 +80,9 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
async getKustoSchema(workspace: string) {
|
||||
const metadata = await this.getMetadata(workspace);
|
||||
return transformMetadataToKustoSchema(metadata, workspace);
|
||||
async getKustoSchema(resourceUri: string) {
|
||||
const metadata = await this.getMetadata(resourceUri);
|
||||
return transformMetadataToKustoSchema(metadata, resourceUri);
|
||||
}
|
||||
|
||||
applyTemplateVariables(target: AzureMonitorQuery, scopedVars: ScopedVars): Record<string, any> {
|
||||
@@ -98,6 +98,8 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
const subscriptionId = templateSrv.replace(target.subscription || this.subscriptionId, scopedVars);
|
||||
const query = templateSrv.replace(item.query, scopedVars, this.interpolateVariable);
|
||||
|
||||
const resource = templateSrv.replace(item.resource, scopedVars);
|
||||
|
||||
return {
|
||||
refId: target.refId,
|
||||
format: target.format,
|
||||
@@ -106,6 +108,9 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
azureLogAnalytics: {
|
||||
resultFormat: item.resultFormat,
|
||||
query: query,
|
||||
resource,
|
||||
|
||||
// TODO: Workspace is deprecated and should be migrated to Resources
|
||||
workspace: workspace,
|
||||
},
|
||||
};
|
||||
@@ -165,6 +170,9 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
|
||||
async getWorkspaceDetails(workspaceId: string) {
|
||||
if (!this.subscriptionId) {
|
||||
return {};
|
||||
}
|
||||
const response = await this.getWorkspaceList(this.subscriptionId);
|
||||
|
||||
const details = response.data.value.find((o: any) => {
|
||||
@@ -236,7 +244,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
'TimeGenerated'
|
||||
);
|
||||
const querystring = querystringBuilder.generate().uriString;
|
||||
const url = `${this.baseUrl}/${workspace}/query?${querystring}`;
|
||||
const url = `${this.baseUrl}/v1/workspaces/${workspace}/query?${querystring}`;
|
||||
const queries: any[] = [];
|
||||
queries.push({
|
||||
datasourceId: this.id,
|
||||
@@ -340,6 +348,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: update to be resource-centric
|
||||
testDatasource(): Promise<any> {
|
||||
const validationError = this.isValidConfig();
|
||||
if (validationError) {
|
||||
@@ -348,7 +357,7 @@ export default class AzureLogAnalyticsDatasource extends DataSourceWithBackend<
|
||||
|
||||
return this.getDefaultOrFirstWorkspace()
|
||||
.then((ws: any) => {
|
||||
const url = `${this.baseUrl}/${ws}/metadata`;
|
||||
const url = `${this.baseUrl}/v1/workspaces/${ws}/metadata`;
|
||||
|
||||
return this.doRequest(url);
|
||||
})
|
||||
|
||||
@@ -2,10 +2,10 @@ import React from 'react';
|
||||
import { AzureMonitorErrorish, AzureMonitorOption, AzureMonitorQuery } from '../../types';
|
||||
import Datasource from '../../datasource';
|
||||
import { InlineFieldRow } from '@grafana/ui';
|
||||
import SubscriptionField from '../SubscriptionField';
|
||||
import WorkspaceField from './WorkspaceField';
|
||||
import QueryField from './QueryField';
|
||||
import FormatAsField from './FormatAsField';
|
||||
import ResourceField from './ResourceField';
|
||||
import useMigrations from './useMigrations';
|
||||
|
||||
interface LogsQueryEditorProps {
|
||||
query: AzureMonitorQuery;
|
||||
@@ -24,18 +24,12 @@ const LogsQueryEditor: React.FC<LogsQueryEditorProps> = ({
|
||||
onChange,
|
||||
setError,
|
||||
}) => {
|
||||
useMigrations(datasource, query, onChange);
|
||||
|
||||
return (
|
||||
<div data-testid="azure-monitor-logs-query-editor">
|
||||
<InlineFieldRow>
|
||||
<SubscriptionField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
variableOptionGroup={variableOptionGroup}
|
||||
onQueryChange={onChange}
|
||||
setError={setError}
|
||||
/>
|
||||
<WorkspaceField
|
||||
<ResourceField
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
subscriptionId={subscriptionId}
|
||||
|
||||
@@ -31,8 +31,12 @@ const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, o
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!query.azureLogAnalytics.resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promises = [
|
||||
datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.workspace),
|
||||
datasource.azureLogAnalyticsDatasource.getKustoSchema(query.azureLogAnalytics.resource),
|
||||
getPromise(),
|
||||
] as const;
|
||||
|
||||
@@ -50,7 +54,7 @@ const QueryField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, o
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics.workspace]);
|
||||
}, [datasource.azureLogAnalyticsDatasource, query.azureLogAnalytics.resource]);
|
||||
|
||||
const handleEditorMount = useCallback((editor: MonacoEditor, monaco: Monaco) => {
|
||||
monacoPromiseRef.current?.resolve?.({ editor, monaco });
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Button, Icon, Modal } from '@grafana/ui';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { AzureQueryEditorFieldProps, AzureResourceSummaryItem } from '../../types';
|
||||
import { Field } from '../Field';
|
||||
import ResourcePicker from '../ResourcePicker';
|
||||
import { parseResourceURI } from '../ResourcePicker/utils';
|
||||
import { Space } from '../Space';
|
||||
|
||||
function parseResourceDetails(resourceURI: string) {
|
||||
const parsed = parseResourceURI(resourceURI);
|
||||
|
||||
if (!parsed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: resourceURI,
|
||||
subscriptionName: parsed.subscriptionID,
|
||||
resourceGroupName: parsed.resourceGroup,
|
||||
name: parsed.resource,
|
||||
};
|
||||
}
|
||||
|
||||
const ResourceField: React.FC<AzureQueryEditorFieldProps> = ({ query, datasource, onQueryChange }) => {
|
||||
const { resource } = query.azureLogAnalytics;
|
||||
|
||||
const [resourceComponents, setResourceComponents] = useState(parseResourceDetails(resource ?? ''));
|
||||
const [pickerIsOpen, setPickerIsOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (resource) {
|
||||
datasource.resourcePickerData.getResource(resource).then(setResourceComponents);
|
||||
} else {
|
||||
setResourceComponents(undefined);
|
||||
}
|
||||
}, [datasource.resourcePickerData, resource]);
|
||||
|
||||
const handleOpenPicker = useCallback(() => {
|
||||
setPickerIsOpen(true);
|
||||
}, []);
|
||||
|
||||
const closePicker = useCallback(() => {
|
||||
setPickerIsOpen(false);
|
||||
}, []);
|
||||
|
||||
const handleApply = useCallback(
|
||||
(resourceURI: string | undefined) => {
|
||||
onQueryChange({
|
||||
...query,
|
||||
azureLogAnalytics: {
|
||||
...query.azureLogAnalytics,
|
||||
resource: resourceURI,
|
||||
},
|
||||
});
|
||||
closePicker();
|
||||
},
|
||||
[closePicker, onQueryChange, query]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal title="Select a resource" isOpen={pickerIsOpen} onDismiss={closePicker}>
|
||||
<ResourcePicker
|
||||
resourcePickerData={datasource.resourcePickerData}
|
||||
resourceURI={query.azureLogAnalytics.resource!}
|
||||
onApply={handleApply}
|
||||
onCancel={closePicker}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<Field label="Resource">
|
||||
<Button variant="secondary" onClick={handleOpenPicker}>
|
||||
{/* Three mutually exclusive states */}
|
||||
{!resource && 'Select a resource'}
|
||||
{resource && resourceComponents && <FormattedResource resource={resourceComponents} />}
|
||||
{resource && !resourceComponents && resource}
|
||||
</Button>
|
||||
</Field>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface FormattedResourceProps {
|
||||
resource: AzureResourceSummaryItem;
|
||||
}
|
||||
|
||||
const FormattedResource: React.FC<FormattedResourceProps> = ({ resource }) => {
|
||||
return (
|
||||
<span>
|
||||
<Icon name="layer-group" /> {resource.subscriptionName}
|
||||
<Separator />
|
||||
<Icon name="folder" /> {resource.resourceGroupName}
|
||||
<Separator />
|
||||
<Icon name="cube" /> {resource.name}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const Separator = () => (
|
||||
<>
|
||||
<Space layout="inline" h={2} />
|
||||
{'/'}
|
||||
<Space layout="inline" h={2} />
|
||||
</>
|
||||
);
|
||||
|
||||
export default ResourceField;
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useEffect } from 'react';
|
||||
import { AzureMonitorQuery } from '../../types';
|
||||
import Datasource from '../../datasource';
|
||||
|
||||
async function migrateWorkspaceQueryToResourceQuery(
|
||||
datasource: Datasource,
|
||||
query: AzureMonitorQuery,
|
||||
onChange: (newQuery: AzureMonitorQuery) => void
|
||||
) {
|
||||
if (query.azureLogAnalytics.workspace !== undefined && !query.azureLogAnalytics.resource) {
|
||||
const resourceURI = await datasource.resourcePickerData.getResourceURIFromWorkspace(
|
||||
query.azureLogAnalytics.workspace
|
||||
);
|
||||
|
||||
const newQuery = {
|
||||
...query,
|
||||
azureLogAnalytics: {
|
||||
...query.azureLogAnalytics,
|
||||
resource: resourceURI,
|
||||
workspace: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
delete newQuery.azureLogAnalytics.workspace;
|
||||
|
||||
onChange(newQuery);
|
||||
}
|
||||
}
|
||||
|
||||
export default function useMigrations(
|
||||
datasource: Datasource,
|
||||
query: AzureMonitorQuery,
|
||||
onChange: (newQuery: AzureMonitorQuery) => void
|
||||
) {
|
||||
useEffect(() => {
|
||||
migrateWorkspaceQueryToResourceQuery(datasource, query, onChange);
|
||||
}, [datasource, query, onChange]);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { cx } from '@emotion/css';
|
||||
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import NestedRows from './NestedRows';
|
||||
import getStyles from './styles';
|
||||
import { Row, RowGroup } from './types';
|
||||
|
||||
interface NestedResourceTableProps {
|
||||
rows: RowGroup;
|
||||
selectedRows: RowGroup;
|
||||
noHeader?: boolean;
|
||||
requestNestedRows: (row: Row) => Promise<void>;
|
||||
onRowSelectedChange: (row: Row, selected: boolean) => void;
|
||||
}
|
||||
|
||||
const NestedResourceTable: React.FC<NestedResourceTableProps> = ({
|
||||
rows,
|
||||
selectedRows,
|
||||
noHeader,
|
||||
requestNestedRows,
|
||||
onRowSelectedChange,
|
||||
}) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<>
|
||||
<table className={styles.table}>
|
||||
{!noHeader && (
|
||||
<thead>
|
||||
<tr className={cx(styles.row, styles.header)}>
|
||||
<td className={styles.cell}>Scope</td>
|
||||
<td className={styles.cell}>Type</td>
|
||||
<td className={styles.cell}>Location</td>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
</table>
|
||||
|
||||
<div className={styles.tableScroller}>
|
||||
<table className={styles.table}>
|
||||
<tbody>
|
||||
<NestedRows
|
||||
rows={rows}
|
||||
selectedRows={selectedRows}
|
||||
level={0}
|
||||
requestNestedRows={requestNestedRows}
|
||||
onRowSelectedChange={onRowSelectedChange}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NestedResourceTable;
|
||||
@@ -0,0 +1,195 @@
|
||||
import { cx } from '@emotion/css';
|
||||
import { Checkbox, HorizontalGroup, Icon, IconButton, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import getStyles from './styles';
|
||||
import { EntryType, Row, RowGroup } from './types';
|
||||
|
||||
interface NestedRowsProps {
|
||||
rows: RowGroup;
|
||||
level: number;
|
||||
selectedRows: RowGroup;
|
||||
requestNestedRows: (row: Row) => Promise<void>;
|
||||
onRowSelectedChange: (row: Row, selected: boolean) => void;
|
||||
}
|
||||
|
||||
const NestedRows: React.FC<NestedRowsProps> = ({
|
||||
rows,
|
||||
selectedRows,
|
||||
level,
|
||||
requestNestedRows,
|
||||
onRowSelectedChange,
|
||||
}) => (
|
||||
<>
|
||||
{Object.keys(rows).map((rowId) => (
|
||||
<NestedRow
|
||||
key={rowId}
|
||||
row={rows[rowId]}
|
||||
selectedRows={selectedRows}
|
||||
level={level}
|
||||
requestNestedRows={requestNestedRows}
|
||||
onRowSelectedChange={onRowSelectedChange}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
interface NestedRowProps {
|
||||
row: Row;
|
||||
level: number;
|
||||
selectedRows: RowGroup;
|
||||
requestNestedRows: (row: Row) => Promise<void>;
|
||||
onRowSelectedChange: (row: Row, selected: boolean) => void;
|
||||
}
|
||||
|
||||
const NestedRow: React.FC<NestedRowProps> = ({ row, selectedRows, level, requestNestedRows, onRowSelectedChange }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const isSelected = !!selectedRows[row.id];
|
||||
const isDisabled = Object.keys(selectedRows).length > 0 && !isSelected;
|
||||
const initialOpenStatus = row.type === EntryType.Collection ? 'open' : 'closed';
|
||||
const [openStatus, setOpenStatus] = useState<'open' | 'closed' | 'loading'>(initialOpenStatus);
|
||||
const isOpen = openStatus === 'open';
|
||||
|
||||
const onRowToggleCollapse = async () => {
|
||||
if (openStatus === 'open') {
|
||||
setOpenStatus('closed');
|
||||
return;
|
||||
}
|
||||
setOpenStatus('loading');
|
||||
await requestNestedRows(row);
|
||||
setOpenStatus('open');
|
||||
};
|
||||
|
||||
// opens the resource group on load of component if there was a previously saved selection
|
||||
useEffect(() => {
|
||||
const selectedRow = Object.keys(selectedRows).map((rowId) => selectedRows[rowId])[0];
|
||||
const isSelectedResourceGroup =
|
||||
selectedRow && selectedRow.resourceGroupName && row.name === selectedRow.resourceGroupName;
|
||||
if (isSelectedResourceGroup) {
|
||||
setOpenStatus('open');
|
||||
}
|
||||
}, [selectedRows, row]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={cx(styles.row, isDisabled && styles.disabledRow)} key={row.id}>
|
||||
<td className={styles.cell}>
|
||||
<NestedEntry
|
||||
level={level}
|
||||
isSelected={isSelected}
|
||||
isDisabled={isDisabled}
|
||||
isOpen={isOpen}
|
||||
entry={row}
|
||||
onToggleCollapse={onRowToggleCollapse}
|
||||
onSelectedChange={onRowSelectedChange}
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td className={styles.cell}>{row.typeLabel}</td>
|
||||
|
||||
<td className={styles.cell}>{row.location ?? '-'}</td>
|
||||
</tr>
|
||||
|
||||
{isOpen && row.children && Object.keys(row.children).length > 0 && (
|
||||
<NestedRows
|
||||
rows={row.children}
|
||||
selectedRows={selectedRows}
|
||||
level={level + 1}
|
||||
requestNestedRows={requestNestedRows}
|
||||
onRowSelectedChange={onRowSelectedChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{openStatus === 'loading' && (
|
||||
<tr>
|
||||
<td className={cx(styles.cell, styles.loadingCell)} colSpan={3}>
|
||||
Loading...
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface EntryIconProps {
|
||||
entry: Row;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const EntryIcon: React.FC<EntryIconProps> = ({ isOpen, entry: { type } }) => {
|
||||
switch (type) {
|
||||
case EntryType.Collection:
|
||||
return <Icon name="layer-group" />;
|
||||
|
||||
case EntryType.SubCollection:
|
||||
return <Icon name={isOpen ? 'folder-open' : 'folder'} />;
|
||||
|
||||
case EntryType.Resource:
|
||||
return <Icon name="cube" />;
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
interface NestedEntryProps {
|
||||
level: number;
|
||||
entry: Row;
|
||||
isSelected: boolean;
|
||||
isOpen: boolean;
|
||||
isDisabled: boolean;
|
||||
onToggleCollapse: (row: Row) => void;
|
||||
onSelectedChange: (row: Row, selected: boolean) => void;
|
||||
}
|
||||
|
||||
const NestedEntry: React.FC<NestedEntryProps> = ({
|
||||
entry,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isOpen,
|
||||
level,
|
||||
onToggleCollapse,
|
||||
onSelectedChange,
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const hasChildren = !!entry.children;
|
||||
const isSelectable = entry.type === EntryType.Resource;
|
||||
|
||||
const handleToggleCollapse = useCallback(() => {
|
||||
onToggleCollapse(entry);
|
||||
}, [onToggleCollapse, entry]);
|
||||
|
||||
const handleSelectedChanged = useCallback(
|
||||
(ev: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const isSelected = ev.target.checked;
|
||||
onSelectedChange(entry, isSelected);
|
||||
},
|
||||
[entry, onSelectedChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ marginLeft: level * (3 * theme.spacing.gridSize) }}>
|
||||
<HorizontalGroup align="center" spacing="sm">
|
||||
{/* When groups are selectable, I *think* we will want to show a 2-wide space instead
|
||||
of the collapse button for leaf rows that have no children to get them to align */}
|
||||
{hasChildren && (
|
||||
<IconButton
|
||||
className={styles.collapseButton}
|
||||
name={isOpen ? 'angle-down' : 'angle-right'}
|
||||
aria-label={isOpen ? 'Collapse' : 'Expand'}
|
||||
onClick={handleToggleCollapse}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isSelectable && <Checkbox onChange={handleSelectedChanged} disabled={isDisabled} value={isSelected} />}
|
||||
|
||||
<EntryIcon entry={entry} isOpen={isOpen} />
|
||||
|
||||
{entry.name}
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NestedRows;
|
||||
@@ -0,0 +1,139 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import NestedResourceTable from './NestedResourceTable';
|
||||
import { Row, RowGroup } from './types';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Button, useStyles2 } from '@grafana/ui';
|
||||
import ResourcePickerData from '../../resourcePicker/resourcePickerData';
|
||||
import { produce } from 'immer';
|
||||
import { Space } from '../Space';
|
||||
import { parseResourceURI } from './utils';
|
||||
|
||||
interface ResourcePickerProps {
|
||||
resourcePickerData: Pick<ResourcePickerData, 'getResourcePickerData' | 'getResourcesForResourceGroup'>;
|
||||
resourceURI: string | undefined;
|
||||
|
||||
onApply: (resourceURI: string | undefined) => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const ResourcePicker = ({ resourcePickerData, resourceURI, onApply, onCancel }: ResourcePickerProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const [rows, setRows] = useState<RowGroup>({});
|
||||
const [internalSelected, setInternalSelected] = useState<string | undefined>(resourceURI);
|
||||
|
||||
useEffect(() => {
|
||||
setInternalSelected(resourceURI);
|
||||
}, [resourceURI]);
|
||||
|
||||
const handleFetchInitialResources = useCallback(async () => {
|
||||
const initalRows = await resourcePickerData.getResourcePickerData();
|
||||
setRows(initalRows);
|
||||
}, [resourcePickerData]);
|
||||
|
||||
useEffect(() => {
|
||||
handleFetchInitialResources();
|
||||
}, [handleFetchInitialResources]);
|
||||
|
||||
const requestNestedRows = useCallback(
|
||||
async (resourceGroup: Row) => {
|
||||
// if we've already fetched resources for a resource group we don't need to re-fetch them
|
||||
if (resourceGroup.children && Object.keys(resourceGroup.children).length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// fetch and set nested resources for the resourcegroup into the bigger state object
|
||||
const resources = await resourcePickerData.getResourcesForResourceGroup(resourceGroup);
|
||||
setRows(
|
||||
produce(rows, (draftState: RowGroup) => {
|
||||
const subscriptionChildren = draftState[resourceGroup.subscriptionId].children;
|
||||
|
||||
if (subscriptionChildren) {
|
||||
subscriptionChildren[resourceGroup.name].children = resources;
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
[resourcePickerData, rows]
|
||||
);
|
||||
|
||||
const handleSelectionChanged = useCallback((row: Row, isSelected: boolean) => {
|
||||
isSelected ? setInternalSelected(row.id) : setInternalSelected(undefined);
|
||||
}, []);
|
||||
|
||||
const selectedResource = useMemo(() => {
|
||||
if (internalSelected && Object.keys(rows).length) {
|
||||
const parsed = parseResourceURI(internalSelected);
|
||||
|
||||
if (parsed) {
|
||||
const { subscriptionID, resourceGroup } = parsed;
|
||||
const allResourceGroups = rows[subscriptionID].children || {};
|
||||
const selectedResourceGroup = allResourceGroups[resourceGroup.toLowerCase()];
|
||||
const allResourcesInResourceGroup = selectedResourceGroup.children;
|
||||
|
||||
if (!allResourcesInResourceGroup || Object.keys(allResourcesInResourceGroup).length === 0) {
|
||||
requestNestedRows(selectedResourceGroup);
|
||||
return {};
|
||||
}
|
||||
|
||||
const matchingResource = allResourcesInResourceGroup[internalSelected];
|
||||
|
||||
return {
|
||||
[internalSelected]: matchingResource,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}, [internalSelected, rows, requestNestedRows]);
|
||||
|
||||
const handleApply = useCallback(() => {
|
||||
onApply(internalSelected);
|
||||
}, [internalSelected, onApply]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NestedResourceTable
|
||||
rows={rows}
|
||||
requestNestedRows={requestNestedRows}
|
||||
onRowSelectedChange={handleSelectionChanged}
|
||||
selectedRows={selectedResource}
|
||||
/>
|
||||
|
||||
<div className={styles.selectionFooter}>
|
||||
{internalSelected && (
|
||||
<>
|
||||
<Space v={2} />
|
||||
<h5>Selection</h5>
|
||||
<NestedResourceTable
|
||||
noHeader={true}
|
||||
rows={selectedResource}
|
||||
requestNestedRows={requestNestedRows}
|
||||
onRowSelectedChange={handleSelectionChanged}
|
||||
selectedRows={selectedResource}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Space v={2} />
|
||||
|
||||
<Button onClick={handleApply}>Apply</Button>
|
||||
<Space layout="inline" h={1} />
|
||||
<Button onClick={onCancel} variant="secondary">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResourcePicker;
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
selectionFooter: css({
|
||||
position: 'sticky',
|
||||
bottom: 0,
|
||||
background: theme.colors.background.primary,
|
||||
paddingTop: theme.spacing(2),
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
table: css({
|
||||
width: '100%',
|
||||
}),
|
||||
|
||||
tableScroller: css({
|
||||
maxHeight: '50vh',
|
||||
overflow: 'auto',
|
||||
}),
|
||||
|
||||
header: css({
|
||||
background: theme.colors.background.secondary,
|
||||
}),
|
||||
|
||||
row: css({
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
|
||||
'&:last-of-type': {
|
||||
borderBottomColor: theme.colors.border.medium,
|
||||
},
|
||||
}),
|
||||
|
||||
disabledRow: css({
|
||||
opacity: 0.5,
|
||||
}),
|
||||
|
||||
cell: css({
|
||||
padding: theme.spacing(1, 0),
|
||||
width: '25%',
|
||||
|
||||
'&:first-of-type': {
|
||||
width: '50%',
|
||||
padding: theme.spacing(1, 0, 1, 2),
|
||||
},
|
||||
}),
|
||||
|
||||
collapseButton: css({ margin: 0 }),
|
||||
|
||||
loadingCell: css({
|
||||
textAlign: 'center',
|
||||
}),
|
||||
});
|
||||
|
||||
export default getStyles;
|
||||
@@ -0,0 +1,19 @@
|
||||
export enum EntryType {
|
||||
Collection,
|
||||
SubCollection,
|
||||
Resource,
|
||||
}
|
||||
export interface Row {
|
||||
id: string;
|
||||
name: string;
|
||||
type: EntryType;
|
||||
typeLabel: string;
|
||||
subscriptionId: string;
|
||||
location?: string;
|
||||
children?: RowGroup;
|
||||
resourceGroupName?: string;
|
||||
}
|
||||
|
||||
export interface RowGroup {
|
||||
[subscriptionIdOrResourceGroupName: string]: Row;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
const RESOURCE_URI_REGEX = /\/subscriptions\/(?<subscriptionID>.+)\/resourceGroups\/(?<resourceGroup>.+)\/providers.+\/(?<resource>[\w-_]+)/;
|
||||
|
||||
export function parseResourceURI(resourceURI: string) {
|
||||
const matches = RESOURCE_URI_REGEX.exec(resourceURI);
|
||||
|
||||
if (!matches?.groups?.subscriptionID || !matches?.groups?.resourceGroup) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { subscriptionID, resourceGroup, resource } = matches.groups;
|
||||
return { subscriptionID, resourceGroup, resource };
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { cloneDeep, upperFirst } from 'lodash';
|
||||
import AzureMonitorDatasource from './azure_monitor/azure_monitor_datasource';
|
||||
import AppInsightsDatasource from './app_insights/app_insights_datasource';
|
||||
import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource';
|
||||
import ResourcePickerData from './resourcePicker/resourcePickerData';
|
||||
import { AzureDataSourceJsonData, AzureMonitorQuery, AzureQueryType, InsightsAnalyticsQuery } from './types';
|
||||
import {
|
||||
DataFrame,
|
||||
@@ -24,6 +25,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
appInsightsDatasource: AppInsightsDatasource;
|
||||
azureLogAnalyticsDatasource: AzureLogAnalyticsDatasource;
|
||||
insightsAnalyticsDatasource: InsightsAnalyticsDatasource;
|
||||
resourcePickerData: ResourcePickerData;
|
||||
azureResourceGraphDatasource: AzureResourceGraphDatasource;
|
||||
|
||||
pseudoDatasource: Record<AzureQueryType, DataSourceWithBackend>;
|
||||
@@ -39,6 +41,7 @@ export default class Datasource extends DataSourceApi<AzureMonitorQuery, AzureDa
|
||||
this.azureLogAnalyticsDatasource = new AzureLogAnalyticsDatasource(instanceSettings);
|
||||
this.insightsAnalyticsDatasource = new InsightsAnalyticsDatasource(instanceSettings);
|
||||
this.azureResourceGraphDatasource = new AzureResourceGraphDatasource(instanceSettings);
|
||||
this.resourcePickerData = new ResourcePickerData(instanceSettings);
|
||||
|
||||
const pseudoDatasource: any = {};
|
||||
pseudoDatasource[AzureQueryType.ApplicationInsights] = this.appInsightsDatasource;
|
||||
|
||||
@@ -183,6 +183,57 @@
|
||||
{ "name": "x-ms-app", "content": "Grafana" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "loganalytics-resourcepickerdata",
|
||||
"method": "POST",
|
||||
"url": "https://management.azure.com",
|
||||
"authType": "azure",
|
||||
"tokenAuth": {
|
||||
"scopes": ["https://management.azure.com/.default"],
|
||||
"params": {
|
||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
||||
"azure_cloud": "AzureCloud",
|
||||
"tenant_id": "{{.JsonData.logAnalyticsTenantId | orEmpty}}",
|
||||
"client_id": "{{.JsonData.logAnalyticsClientId | orEmpty}}",
|
||||
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret | orEmpty}}"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "loganalytics-resourcepickerdata-china",
|
||||
"method": "POST",
|
||||
"url": "https://management.chinacloudapi.cn",
|
||||
"authType": "azure",
|
||||
"tokenAuth": {
|
||||
"scopes": ["https://management.chinacloudapi.cn/.default"],
|
||||
"params": {
|
||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
||||
"azure_cloud": "AzureChinaCloud",
|
||||
"tenant_id": "{{.JsonData.logAnalyticsTenantId | orEmpty}}",
|
||||
"client_id": "{{.JsonData.logAnalyticsClientId | orEmpty}}",
|
||||
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret | orEmpty}}"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "loganalytics-resourcepickerdata-gov",
|
||||
"method": "POST",
|
||||
"url": "https://management.usgovcloudapi.net",
|
||||
"authType": "azure",
|
||||
"tokenAuth": {
|
||||
"scopes": ["https://management.usgovcloudapi.net/.default"],
|
||||
"params": {
|
||||
"azure_auth_type": "{{.JsonData.azureAuthType | orEmpty}}",
|
||||
"azure_cloud": "AzureUSGovernment",
|
||||
"tenant_id": "{{.JsonData.logAnalyticsTenantId | orEmpty}}",
|
||||
"client_id": "{{.JsonData.logAnalyticsClientId | orEmpty}}",
|
||||
"client_secret": "{{.SecureJsonData.logAnalyticsClientSecret | orEmpty}}"
|
||||
}
|
||||
},
|
||||
"headers": [{ "name": "x-ms-app", "content": "Grafana" }]
|
||||
},
|
||||
{
|
||||
"path": "workspacesloganalytics",
|
||||
"method": "GET",
|
||||
@@ -237,7 +288,7 @@
|
||||
{
|
||||
"path": "loganalyticsazure",
|
||||
"method": "GET",
|
||||
"url": "https://api.loganalytics.io/v1/workspaces",
|
||||
"url": "https://api.loganalytics.io/",
|
||||
"authType": "azure",
|
||||
"tokenAuth": {
|
||||
"scopes": ["https://api.loganalytics.io/.default"],
|
||||
@@ -257,7 +308,7 @@
|
||||
{
|
||||
"path": "chinaloganalyticsazure",
|
||||
"method": "GET",
|
||||
"url": "https://api.loganalytics.azure.cn/v1/workspaces",
|
||||
"url": "https://api.loganalytics.azure.cn/",
|
||||
"authType": "azure",
|
||||
"tokenAuth": {
|
||||
"scopes": ["https://api.loganalytics.azure.cn/.default"],
|
||||
@@ -277,7 +328,7 @@
|
||||
{
|
||||
"path": "govloganalyticsazure",
|
||||
"method": "GET",
|
||||
"url": "https://api.loganalytics.us/v1/workspaces",
|
||||
"url": "https://api.loganalytics.us/",
|
||||
"authType": "azure",
|
||||
"tokenAuth": {
|
||||
"scopes": ["https://api.loganalytics.us/.default"],
|
||||
|
||||
@@ -34,9 +34,9 @@ export class AzureMonitorQueryCtrl extends QueryCtrl {
|
||||
reactQueryEditors = [
|
||||
AzureQueryType.AzureMonitor,
|
||||
AzureQueryType.LogAnalytics,
|
||||
AzureQueryType.ApplicationInsights,
|
||||
AzureQueryType.InsightsAnalytics,
|
||||
AzureQueryType.AzureResourceGraph,
|
||||
// AzureQueryType.ApplicationInsights,
|
||||
// AzureQueryType.InsightsAnalytics,
|
||||
];
|
||||
|
||||
// target: AzureMonitorQuery;
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
import { FetchResponse, getBackendSrv } from '@grafana/runtime';
|
||||
import { getLogAnalyticsResourcePickerApiRoute } from '../api/routes';
|
||||
import { EntryType, Row, RowGroup } from '../components/ResourcePicker/types';
|
||||
import { getAzureCloud } from '../credentials';
|
||||
import { AzureDataSourceInstanceSettings, AzureResourceSummaryItem } from '../types';
|
||||
import { SUPPORTED_LOCATIONS, SUPPORTED_RESOURCE_TYPES } from './supportedResources';
|
||||
|
||||
const RESOURCE_GRAPH_URL = '/providers/Microsoft.ResourceGraph/resources?api-version=2020-04-01-preview';
|
||||
|
||||
interface RawAzureResourceGroupItem {
|
||||
subscriptionId: string;
|
||||
subscriptionName: string;
|
||||
resourceGroup: string;
|
||||
resourceGroupId: string;
|
||||
}
|
||||
|
||||
interface RawAzureResourceItem {
|
||||
id: string;
|
||||
name: string;
|
||||
subscriptionId: string;
|
||||
resourceGroup: string;
|
||||
type: string;
|
||||
location: string;
|
||||
}
|
||||
|
||||
interface AzureGraphResponse<T = unknown> {
|
||||
data: T;
|
||||
}
|
||||
|
||||
export default class ResourcePickerData {
|
||||
private proxyUrl: string;
|
||||
private cloud: string;
|
||||
|
||||
constructor(instanceSettings: AzureDataSourceInstanceSettings) {
|
||||
this.proxyUrl = instanceSettings.url!;
|
||||
this.cloud = getAzureCloud(instanceSettings);
|
||||
}
|
||||
|
||||
async getResourcePickerData() {
|
||||
const { ok, data: response } = await this.makeResourceGraphRequest<RawAzureResourceGroupItem[]>(
|
||||
`resources
|
||||
| join kind=leftouter (ResourceContainers | where type=='microsoft.resources/subscriptions' | project subscriptionName=name, subscriptionId, resourceGroupId=id) on subscriptionId
|
||||
| where type in (${SUPPORTED_RESOURCE_TYPES})
|
||||
| summarize count() by resourceGroup, subscriptionName, resourceGroupId, subscriptionId
|
||||
| order by resourceGroup asc
|
||||
`
|
||||
);
|
||||
|
||||
// TODO: figure out desired error handling strategy
|
||||
if (!ok) {
|
||||
throw new Error('unable to fetch resource containers');
|
||||
}
|
||||
|
||||
return this.formatResourceGroupData(response.data);
|
||||
}
|
||||
|
||||
async getResourcesForResourceGroup(resourceGroup: Row) {
|
||||
const { ok, data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
|
||||
resources
|
||||
| where resourceGroup == "${resourceGroup.name.toLowerCase()}"
|
||||
| where type in (${SUPPORTED_RESOURCE_TYPES}) and location in (${SUPPORTED_LOCATIONS})
|
||||
`);
|
||||
|
||||
// TODO: figure out desired error handling strategy
|
||||
if (!ok) {
|
||||
throw new Error('unable to fetch resource containers');
|
||||
}
|
||||
|
||||
return this.formatResourceGroupChildren(response.data);
|
||||
}
|
||||
|
||||
async getResource(resourceURI: string) {
|
||||
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
|
||||
`;
|
||||
|
||||
const { ok, data: response } = await this.makeResourceGraphRequest<AzureResourceSummaryItem[]>(query);
|
||||
|
||||
if (!ok || !response.data[0]) {
|
||||
throw new Error('unable to fetch resource details');
|
||||
}
|
||||
|
||||
return response.data[0];
|
||||
}
|
||||
|
||||
async getResourceURIFromWorkspace(workspace: string) {
|
||||
const { ok, data: response } = await this.makeResourceGraphRequest<RawAzureResourceItem[]>(`
|
||||
resources
|
||||
| where properties['customerId'] == "${workspace}"
|
||||
| project id
|
||||
`);
|
||||
|
||||
// TODO: figure out desired error handling strategy
|
||||
if (!ok) {
|
||||
throw new Error('unable to fetch resource containers');
|
||||
}
|
||||
|
||||
return response.data[0].id;
|
||||
}
|
||||
|
||||
formatResourceGroupData(rawData: RawAzureResourceGroupItem[]) {
|
||||
const formatedSubscriptionsAndResourceGroups: RowGroup = {};
|
||||
|
||||
rawData.forEach((resourceGroup) => {
|
||||
// if the subscription doesn't exist yet, create it
|
||||
if (!formatedSubscriptionsAndResourceGroups[resourceGroup.subscriptionId]) {
|
||||
formatedSubscriptionsAndResourceGroups[resourceGroup.subscriptionId] = {
|
||||
name: resourceGroup.subscriptionName,
|
||||
id: resourceGroup.subscriptionId,
|
||||
subscriptionId: resourceGroup.subscriptionId,
|
||||
typeLabel: 'Subscription',
|
||||
type: EntryType.Collection,
|
||||
children: {},
|
||||
};
|
||||
}
|
||||
|
||||
// add the resource group to the subscription
|
||||
// store by resourcegroupname not id to match resource uri
|
||||
(formatedSubscriptionsAndResourceGroups[resourceGroup.subscriptionId].children as RowGroup)[
|
||||
resourceGroup.resourceGroup
|
||||
] = {
|
||||
name: resourceGroup.resourceGroup,
|
||||
id: resourceGroup.resourceGroupId,
|
||||
subscriptionId: resourceGroup.subscriptionId,
|
||||
type: EntryType.SubCollection,
|
||||
typeLabel: 'Resource Group',
|
||||
children: {},
|
||||
};
|
||||
});
|
||||
|
||||
return formatedSubscriptionsAndResourceGroups;
|
||||
}
|
||||
|
||||
formatResourceGroupChildren(rawData: RawAzureResourceItem[]) {
|
||||
const children: RowGroup = {};
|
||||
|
||||
rawData.forEach((item: RawAzureResourceItem) => {
|
||||
children[item.id] = {
|
||||
name: item.name,
|
||||
id: item.id,
|
||||
subscriptionId: item.id,
|
||||
resourceGroupName: item.resourceGroup,
|
||||
type: EntryType.Resource,
|
||||
typeLabel: item.type, // TODO: these types can be quite long, we may wish to format them more
|
||||
location: item.location, // TODO: we may wish to format these locations, by default they are written as 'northeurope' rather than a more human readable "North Europe"
|
||||
};
|
||||
});
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
async makeResourceGraphRequest<T = unknown>(
|
||||
query: string,
|
||||
maxRetries = 1
|
||||
): Promise<FetchResponse<AzureGraphResponse<T>>> {
|
||||
try {
|
||||
return await getBackendSrv()
|
||||
.fetch<AzureGraphResponse<T>>({
|
||||
url: this.proxyUrl + '/' + getLogAnalyticsResourcePickerApiRoute(this.cloud) + RESOURCE_GRAPH_URL,
|
||||
method: 'POST',
|
||||
data: {
|
||||
query: query,
|
||||
options: {
|
||||
resultFormat: 'objectArray',
|
||||
},
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
} catch (error) {
|
||||
if (maxRetries > 0) {
|
||||
return this.makeResourceGraphRequest(query, maxRetries - 1);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// TODO: should this list be different for logs vs metrics?
|
||||
export const SUPPORTED_RESOURCE_TYPES = [
|
||||
'microsoft.analysisservices/servers',
|
||||
'microsoft.apimanagement/service',
|
||||
'microsoft.network/applicationgateways',
|
||||
'microsoft.insights/components',
|
||||
'microsoft.web/hostingenvironments',
|
||||
'microsoft.web/serverfarms',
|
||||
'microsoft.web/sites',
|
||||
'microsoft.automation/automationaccounts',
|
||||
'microsoft.botservice/botservices',
|
||||
'microsoft.appplatform/spring',
|
||||
'microsoft.network/bastionhosts',
|
||||
'microsoft.batch/batchaccounts',
|
||||
'microsoft.cdn/cdnwebapplicationfirewallpolicies',
|
||||
'microsoft.classiccompute/domainnames',
|
||||
'microsoft.classiccompute/virtualmachines',
|
||||
'microsoft.vmwarecloudsimple/virtualmachines',
|
||||
'microsoft.cognitiveservices/accounts',
|
||||
'microsoft.appconfiguration/configurationstores',
|
||||
'microsoft.network/connections',
|
||||
'microsoft.containerinstance/containergroups',
|
||||
'microsoft.containerregistry/registries',
|
||||
'microsoft.containerservice/managedclusters',
|
||||
'microsoft.documentdb/databaseaccounts',
|
||||
'microsoft.databoxedge/databoxedgedevices',
|
||||
'microsoft.datafactory/datafactories',
|
||||
'microsoft.datafactory/factories',
|
||||
'microsoft.datalakeanalytics/accounts',
|
||||
'microsoft.datalakestore/accounts',
|
||||
'microsoft.datashare/accounts',
|
||||
'microsoft.dbformysql/servers',
|
||||
'microsoft.devices/provisioningservices',
|
||||
'microsoft.compute/disks',
|
||||
'microsoft.network/dnszones',
|
||||
'microsoft.eventgrid/domains',
|
||||
'microsoft.eventgrid/topics',
|
||||
'microsoft.eventgrid/systemtopics',
|
||||
'microsoft.eventhub/namespaces',
|
||||
'microsoft.eventhub/clusters',
|
||||
'microsoft.network/expressroutecircuits',
|
||||
'microsoft.network/expressrouteports',
|
||||
'microsoft.network/azurefirewalls',
|
||||
'microsoft.network/frontdoors',
|
||||
'microsoft.hdinsight/clusters',
|
||||
'microsoft.iotcentral/iotapps',
|
||||
'microsoft.devices/iothubs',
|
||||
'microsoft.keyvault/vaults',
|
||||
'microsoft.kubernetes/connectedclusters',
|
||||
'microsoft.kusto/clusters',
|
||||
'microsoft.network/loadbalancers',
|
||||
'microsoft.operationalinsights/workspaces',
|
||||
'microsoft.logic/workflows',
|
||||
'microsoft.logic/integrationserviceenvironments',
|
||||
'microsoft.machinelearningservices/workspaces',
|
||||
'microsoft.dbformariadb/servers',
|
||||
'microsoft.media/mediaservices',
|
||||
'microsoft.media/mediaservices/streamingendpoints',
|
||||
'microsoft.network/natgateways',
|
||||
'microsoft.netapp/netappaccounts/capacitypools',
|
||||
'microsoft.netapp/netappaccounts/capacitypools/volumes',
|
||||
'microsoft.network/networkinterfaces',
|
||||
'microsoft.notificationhubs/namespaces/notificationhubs',
|
||||
'microsoft.peering/peeringservices',
|
||||
'microsoft.dbforpostgresql/servers',
|
||||
'microsoft.dbforpostgresql/serversv2',
|
||||
'microsoft.powerbidedicated/capacities',
|
||||
'microsoft.network/privateendpoints',
|
||||
'microsoft.network/privatelinkservices',
|
||||
'microsoft.network/publicipaddresses',
|
||||
'microsoft.cache/redis',
|
||||
'microsoft.cache/redisenterprise',
|
||||
'microsoft.relay/namespaces',
|
||||
'microsoft.search/searchservices',
|
||||
'microsoft.dbforpostgresql/servergroupsv2',
|
||||
'microsoft.servicebus/namespaces',
|
||||
'microsoft.signalrservice/signalr',
|
||||
'microsoft.operationsmanagement/solutions',
|
||||
'microsoft.sql/managedinstances',
|
||||
'microsoft.sql/servers/databases',
|
||||
'microsoft.sql/servers/elasticpools',
|
||||
'microsoft.storage/storageaccounts',
|
||||
'microsoft.storagecache/caches',
|
||||
'microsoft.classicstorage/storageaccounts',
|
||||
'microsoft.storagesync/storagesyncservices',
|
||||
'microsoft.streamanalytics/streamingjobs',
|
||||
'microsoft.synapse/workspaces',
|
||||
'microsoft.synapse/workspaces/bigdatapools',
|
||||
'microsoft.synapse/workspaces/scopepools',
|
||||
'microsoft.synapse/workspaces/sqlpools',
|
||||
'microsoft.timeseriesinsights/environments',
|
||||
'microsoft.network/trafficmanagerprofiles',
|
||||
'microsoft.compute/virtualmachines',
|
||||
'microsoft.compute/virtualmachinescalesets',
|
||||
'microsoft.network/virtualnetworkgateways',
|
||||
'microsoft.web/sites/slots',
|
||||
'microsoft.resources/subscriptions',
|
||||
'microsoft.insights/autoscalesettings',
|
||||
'microsoft.aadiam/azureadmetrics',
|
||||
'microsoft.azurestackresourcemonitor/storageaccountmonitor',
|
||||
'microsoft.network/networkwatchers/connectionmonitors',
|
||||
'microsoft.customerinsights/hubs',
|
||||
'microsoft.insights/qos',
|
||||
'microsoft.network/expressroutegateways',
|
||||
'microsoft.fabric.admin/fabriclocations',
|
||||
'microsoft.network/networkvirtualappliances',
|
||||
'microsoft.media/mediaservices/liveevents',
|
||||
'microsoft.network/networkwatchers',
|
||||
'microsoft.network/p2svpngateways',
|
||||
'microsoft.dbforpostgresql/flexibleservers',
|
||||
'microsoft.network/vpngateways',
|
||||
'microsoft.web/hostingenvironments/workerpools',
|
||||
]
|
||||
.map((type) => `"${type}"`)
|
||||
.join(',');
|
||||
|
||||
export const SUPPORTED_LOCATIONS = [
|
||||
'eastus',
|
||||
'eastus2',
|
||||
'southcentralus',
|
||||
'westus2',
|
||||
'westus3',
|
||||
'australiaeast',
|
||||
'southeastasia',
|
||||
'northeurope',
|
||||
'uksouth',
|
||||
'westeurope',
|
||||
'centralus',
|
||||
'northcentralus',
|
||||
'westus',
|
||||
'southafricanorth',
|
||||
'centralindia',
|
||||
'eastasia',
|
||||
'japaneast',
|
||||
'jioindiawest',
|
||||
'koreacentral',
|
||||
'canadacentral',
|
||||
'francecentral',
|
||||
'germanywestcentral',
|
||||
'norwayeast',
|
||||
'switzerlandnorth',
|
||||
'uaenorth',
|
||||
'brazilsouth',
|
||||
'centralusstage',
|
||||
'eastusstage',
|
||||
'eastus2stage',
|
||||
'northcentralusstage',
|
||||
'southcentralusstage',
|
||||
'westusstage',
|
||||
'westus2stage',
|
||||
'asia',
|
||||
'asiapacific',
|
||||
'australia',
|
||||
'brazil',
|
||||
'canada',
|
||||
'europe',
|
||||
'global',
|
||||
'india',
|
||||
'japan',
|
||||
'uk',
|
||||
'unitedstates',
|
||||
'eastasiastage',
|
||||
'southeastasiastage',
|
||||
'westcentralus',
|
||||
'southafricawest',
|
||||
'australiacentral',
|
||||
'australiacentral2',
|
||||
'australiasoutheast',
|
||||
'japanwest',
|
||||
'koreasouth',
|
||||
'southindia',
|
||||
'westindia',
|
||||
'canadaeast',
|
||||
'francesouth',
|
||||
'germanynorth',
|
||||
'norwaywest',
|
||||
'switzerlandwest',
|
||||
'ukwest',
|
||||
'uaecentral',
|
||||
'brazilsoutheast',
|
||||
]
|
||||
.map((type) => `"${type}"`)
|
||||
.join(',');
|
||||
@@ -91,7 +91,10 @@ export interface AzureMetricQuery {
|
||||
export interface AzureLogsQuery {
|
||||
query: string;
|
||||
resultFormat: string;
|
||||
workspace: string;
|
||||
resource?: string;
|
||||
|
||||
/** @deprecated Queries should be migrated to use Resource instead */
|
||||
workspace?: string;
|
||||
}
|
||||
|
||||
export interface AzureResourceGraphQuery {
|
||||
@@ -193,3 +196,10 @@ export interface AzureQueryEditorFieldProps {
|
||||
onQueryChange: (newQuery: AzureMonitorQuery) => void;
|
||||
setError: (source: string, error: AzureMonitorErrorish | undefined) => void;
|
||||
}
|
||||
|
||||
export interface AzureResourceSummaryItem {
|
||||
id: string;
|
||||
name: string;
|
||||
subscriptionName: string;
|
||||
resourceGroupName: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user